You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 09:05:16