使用Retrofit和Kotlin调用OCR API传图遇HTTP 400错误求助
调用ImageToText接口持续收到HTTP 400错误的排查与解决
问题描述
尝试调用api-ninjas的imagetotext接口上传图片提取文本,但始终收到HTTP 400错误,无法定位原因。图片存放在assets目录,根据接口文档应返回单词列表。相关代码如下:
数据类定义
data class Word( val bounding_box: BoundingBox, val text: String ) data class BoundingBox( val x1: Int, val x2: Int, val y1: Int, val y2: Int )
FileApi.kt
import com.example.misinfo.retrofitfileupload.dto.Word import okhttp3.MultipartBody import okhttp3.OkHttpClient import retrofit2.Call import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import retrofit2.http.Multipart import retrofit2.http.POST import retrofit2.http.Part interface FileApi { @Multipart @POST("/v1/imagetotext") suspend fun uploadImage( @Part image: MultipartBody.Part, ): Call<List<Word>> companion object { private val client = OkHttpClient.Builder().build() val instance by lazy { Retrofit.Builder() .baseUrl("https://api.api-ninjas.com/") .client(client) .addConverterFactory(GsonConverterFactory.create()) .build() .create(FileApi::class.java) } } }
FileRepository.kt
import android.util.Log import com.example.misinfo.retrofitfileupload.dto.Word import okhttp3.MultipartBody import java.io.File import java.io.IOException import okhttp3.RequestBody.Companion.asRequestBody import retrofit2.Call import retrofit2.Callback import retrofit2.HttpException import retrofit2.Response class FileRepository { suspend fun uploadImage(file: File): Boolean { var res = false try { FileApi.instance.uploadImage( image = MultipartBody.Part .createFormData( "image", file.name, file.asRequestBody() ) ).enqueue( object : Callback<List<Word>> { override fun onFailure(call: Call<List<Word>>, t: Throwable) { Log.e("API Request", "I got an error and i don't know why :(") } override fun onResponse( call: Call<List<Word>>, response: Response<List<Word>>) { val responseBody = response.body() Log.e("API Request", responseBody.toString()) } } ) res = true return res } catch (e: IOException) { e.printStackTrace() return res } catch (e: HttpException) { e.printStackTrace() return res } finally { Log.e("API Request", res.toString()) } } }
FileViewModel.kt
import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.example.misinfo.retrofitfileupload.FileRepository import kotlinx.coroutines.launch import java.io.File class FileViewModel( private val repository: FileRepository= FileRepository() ): ViewModel() { fun uploadImage(file: File){ viewModelScope.launch { repository.uploadImage(file) } } }
MainActivity.kt
class MainActivity : ComponentActivity() { class MyReceiver : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { println("Clicked on Pip Action") } } private val isPipSupported by lazy { packageManager.hasSystemFeature( PackageManager.FEATURE_PICTURE_IN_PICTURE ) } private var viewBounds = Rect() @RequiresApi(Build.VERSION_CODES.O) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MisInfoTheme { val viewModel = viewModel<FileViewModel>() Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Button(onClick = { val file = File(cacheDir, "img.png") file.createNewFile() file.outputStream().use { assets.open("img.png").copyTo(it) } viewModel.uploadImage(file) }) { Text(text = "Upload image") } } } } } }
排查与解决提示
1. 必须添加API密钥请求头
api-ninjas的所有接口都要求携带X-Api-Key请求头,这是导致400错误的最常见原因。修改OkHttpClient的构建逻辑:
private val client = OkHttpClient.Builder() .addInterceptor { chain -> val originalRequest = chain.request() val authorizedRequest = originalRequest.newBuilder() .header("X-Api-Key", "你的实际API密钥") // 替换为你在api-ninjas获取的密钥 .build() chain.proceed(authorizedRequest) } .build()
2. 修复协程与Retrofit Call的混用问题
你的uploadImage方法标记为suspend,但内部使用了enqueue异步回调,这会导致逻辑混乱(比如res = true会在请求未完成时就返回)。正确的做法是使用Retrofit的挂起函数特性,去掉Call包装:
- 修改FileApi接口方法:
@Multipart @POST("/v1/imagetotext") suspend fun uploadImage( @Part image: MultipartBody.Part, ): List<Word>
- 修改FileRepository的实现:
suspend fun uploadImage(file: File): Boolean { return try { val result = FileApi.instance.uploadImage( image = MultipartBody.Part.createFormData( "image", file.name, file.asRequestBody("image/png".toMediaType()) ) ) Log.e("API Request", "提取结果:$result") true } catch (e: IOException) { Log.e("API Request", "网络异常:${e.message}") e.printStackTrace() false } catch (e: HttpException) { Log.e("API Request", "HTTP错误:${e.code()},信息:${e.message()}") e.printStackTrace() false } }
3. 指定图片MIME类型
file.asRequestBody()未指定MIME类型,接口可能无法识别图片格式导致400。添加对应格式的MIME类型(示例为png,根据你的图片格式调整):
file.asRequestBody("image/png".toMediaType())
需要导入OkHttp的MediaType扩展:
import okhttp3.MediaType.Companion.toMediaType
4. 添加日志拦截器排查请求细节
通过OkHttp日志拦截器查看完整的请求和响应内容,包括请求头、请求体和错误响应,能精准定位问题:
- 先添加依赖(在Module级build.gradle的dependencies中):
implementation "com.squareup.okhttp3:logging-interceptor:4.11.0"
- 配置到OkHttpClient:
val loggingInterceptor = HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY // 打印完整请求响应 } private val client = OkHttpClient.Builder() .addInterceptor(loggingInterceptor) .addInterceptor { chain -> // API密钥拦截器逻辑 } .build()
5. 验证文件完整性
检查从assets复制到cacheDir的文件是否完整,可打印文件大小对比:
val assetSize = assets.open("img.png").use { it.available() } val copiedFileSize = file.length() Log.e("FileCheck", "原文件大小:$assetSize,复制后:$copiedFileSize")
如果大小不一致,说明文件复制过程出错,需修复复制逻辑。
内容的提问来源于stack exchange,提问作者masinnlos
相关产品推荐
相关产品推荐

