改造 OkHttp - "unexpected end of stream"

Retrofit OkHttp - "unexpected end of stream"

我在将 Retrofit (2.9.0) 与 OkHttp3 (4.9.1) 结合使用时收到“流的意外结束”

改造配置:

interface ApiServiceInterface {

    companion object Factory{

        fun create(): ApiServiceInterface {
            val interceptor = HttpLoggingInterceptor()
            interceptor.level = HttpLoggingInterceptor.Level.BODY

            val client = OkHttpClient.Builder()
                .connectTimeout(30, TimeUnit.SECONDS)
                .writeTimeout(30, TimeUnit.SECONDS)
                .readTimeout(30,TimeUnit.SECONDS)
                .addInterceptor(Interceptor { chain ->
                    chain.request().newBuilder()
                        .addHeader("Connection", "close")
                        .addHeader("Accept-Encoding", "identity")
                        .build()
                        .let(chain::proceed)
                })
                .retryOnConnectionFailure(true)
                .connectionPool(ConnectionPool(0, 5, TimeUnit.MINUTES))
                .protocols(listOf(Protocol.HTTP_1_1))
                .build()
            val gson = GsonBuilder().setLenient().create()
            val retrofit = Retrofit.Builder()
                    .addCallAdapterFactory(CoroutineCallAdapterFactory())
                    .addConverterFactory(GsonConverterFactory.create(gson))
                    .baseUrl("http://***.***.***.***:****")
                    .client(client)
                    .build()
            return retrofit.create(ApiServiceInterface::class.java)
        }
    }


    @Headers("Content-type: application/json", "Connection: close", "Accept-Encoding: identity")
    @POST("/")
    fun requestAsync(@Body data: JsonObject): Deferred<Response>
}

到目前为止,我发现了以下内容:

  1. 这个问题只发生在我使用 Android Studio 模拟器 运行 来自 Windows 系列 OS (7, 10, 11) - 这是在 2来自不同网络的不同笔记本电脑。
  2. 如果 运行 Android Studio 模拟器在 OS X 下,问题将不会在 100% 的情况下重现。
  3. ARC/Postman 客户端在完成对我后端的相同请求时从来没有任何问题。
  4. 在 Windows Android Studio 模拟器的 运行 上,此问题在大约 10-50% 的请求中重现,其他请求可以正常工作。
  5. 相同的请求可能会导致此错误或成功完成。
  6. 需要大约 11 秒才能完成的响应可能会导致成功,而需要大约 100 毫秒才能完成的响应可能会导致此错误。
  7. 从改造配置中注释掉 .client(client) 消除了这个问题,但我失去了使用拦截器和其他 OkHttp 功能的机会。
  8. 添加headers(连接:关闭,Accept-Encoding:身份)不能解决问题。
  9. 打开或关闭 retryOnConnectionFailure 对问题也没有影响。
  10. 更改 HttpLoggingInterceptor 级别或将其完全删除并不能解决问题。

Server-side配置:

const http = require('http');
const server = http.createServer((req, res) => {

    const callback = function(code, request, data) {
        let result = responser(code, request, data);
        res.writeHead(200, {
            'Content-Type' : 'x-application/json',
            'Connection': 'close',
            'Content-Length': Buffer.byteLength(result)
        });
        res.end(result);
    };

    ...
}

server.listen(process.env.PORT, process.env.HOSTNAME, () => {
    console.log(`Server is running`);
});

因此,基于 123 - 这不太可能 server-side 问题。
基于 456 - 这不是格式错误的请求相关或执行时间相关的问题。 来自 7 的猜测 - 这个问题的根源在于 OkHttp 而不是 Retrofit 本身。

我读了将近一半的 Whosebug 都是搜索解决方案,比如:


以及 OkHttp 上的讨论 Github:
https://github.com/square/okhttp/issues/3682
https://github.com/square/okhttp/issues/3715
但到目前为止没有任何帮助。

知道可能导致问题的原因吗?

更新

我有更多的情况信息。

首先,我将后端的 headers 更改为不传递 Content-Length 而是传递 Transfer-Encoding : identity。我不知道为什么,但是如果 theese headers 都存在,Postman 会报错,说这是不对的。

res.writeHead(200, {
            'Content-Type' : 'x-application/json',
            'Connection': 'close',
            'Transfer-Encoding': 'identity'
        });

之后,我开始在 Windows 托管的 Android Studio 模拟器上收到另一个错误(失败/成功与“流的意外结束”的比例相等)

2021-12-09 14:58:19.696 401-401/? D/P2P-> FRG DEBUG:: java.io.EOFException: End of input at line 1 column 1807 path $.meta
        at com.google.gson.stream.JsonReader.nextNonWhitespace(JsonReader.java:1397)
        at com.google.gson.stream.JsonReader.doPeek(JsonReader.java:483)
        at com.google.gson.stream.JsonReader.hasNext(JsonReader.java:415)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:216)
        at retrofit2.converter.gson.GsonResponseBodyConverter.convert(GsonResponseBodyConverter.java:40)
        at retrofit2.converter.gson.GsonResponseBodyConverter.convert(GsonResponseBodyConverter.java:27)
        at retrofit2.OkHttpCall.parseResponse(OkHttpCall.java:243)
        at retrofit2.OkHttpCall.onResponse(OkHttpCall.java:153)
        at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:519)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:764)

花了很多时间调试这个问题,我发现这个异常是由 JsonReader.java 在方法 nextNonWhitespace 中生成的,它试图获取冒号、双引号和大括号或方括号从解码为 char 数组缓冲区组成 json object。

此缓冲区本身在同一模块的 fillBuffer 方法中接收,并且其长度限制为 1024 个元素。在我的例子中,后端响应比这个值(1807 个字符)长,所以虽然 JsonReader.java 将我的响应解析为 json object 它在 2 次迭代中执行此操作。

每次迭代都会在此处填充缓冲区:

int total;
while ((total = in.read(buffer, limit, buffer.length - limit)) != -1) {
    limit += total;

    // if this is the first read, consume an optional byte order mark (BOM) if it exists
    if (lineNumber == 0 && lineStart == 0 && limit > 0 && buffer[0] == '\ufeff') {
        pos++;
        lineStart++;
        minimum++;
    }

    if (limit >= minimum) {
        return true;
    }
}

read 方法在 ResponseBody.kt class 上从 okhttp3

调用
@Throws(IOException::class)
    override fun read(cbuf: CharArray, off: Int, len: Int): Int {
      if (closed) throw IOException("Stream closed")

      val finalDelegate = delegate ?: InputStreamReader(
          source.inputStream(),
          source.readBomAsCharset(charset)).also {
        delegate = it
      }
      return finalDelegate.read(cbuf, off, len)
    }

主要问题是:

第一次迭代一切顺利,ResponseBody.kt“读取”前 1024 个字符并将它们提供给 JsonReader.java,它构成响应的一部分 object。

当第二次迭代到来时ResponseBody.kt“读取”响应的最后部分并用它填充 char 缓冲区的开头,因此 char 缓冲区现在包含响应的尾部作为其第一个元素,然后是 -第一次迭代后保留在那里的所有元素。

主要问题是,在大多数情况下(大约 80%)从响应中丢失最后一个字符,在大约 10% 中从响应中丢失最后 2 个字符,在大约 10% 中它读取所有字符。这是截图:

它必须包含 783 个字符才能完成 json,但如第 1290 行所示,它只收到 782.

查看缓冲区本身
782 索引处的字符(按顺序为 783)必须是关闭 json 根的第二个大括号,但取而代之的是第一次迭代开始时的剩余部分。这导致上述异常。

现在,如果我们查看请求成功完成的情况: 对于相同的请求,它偶尔 returns 有效字符数:783

缓冲区本身是: 现在第二个大括号出现在它必须出现的地方。

在这种情况下请求会成功。

相同的响应以 Postman 结尾: Postman 解析响应成功率为 100%,OS X hosted android studio 模拟器和我用过的真实设备也是如此。

更新 2

似乎在 RealBufferedSource.kt:

中获得了完整的缓冲区
internal inline fun RealBufferedSource.commonSelect(options: Options): Int {
  check(!closed) { "closed" }

  while (true) {
    val index = buffer.selectPrefix(options, selectTruncated = true)
    when (index) {
      -1 -> {
        return -1
      }
      -2 -> {
        // We need to grow the buffer. Do that, then try it all again.
        if (source.read(buffer, Segment.SIZE.toLong()) == -1L) return -1
      }
      else -> {
        // We matched a full byte string: consume it and return it.
        val selectedSize = options.byteStrings[index].size
        buffer.skip(selectedSize.toLong())
        return index
      }
    }
  }
}

这里已经缺少最后一个字符:

更新 3

找到了这个未解决的问题,这正是 s我的行为:
Retrofit Json data truncated
还有来自 Android Studio 仿真器问题跟踪器的评论:
https://issuetracker.google.com/issues/119027639#comment9

好的,这花了一些时间,但我发现了问题所在以及解决方法。

当 Android Studio 的模拟器 运行 在 Windows 系列 OS 中(检查 7 和 10)收到来自服务器的 json-typed 回复与改造它可以当 body 解码为字符串时,以各种概率松散 1 或 2 个最后符号,该符号包含右花括号,因此 gson 转换器无法将 body 解析为 object这会导致抛出异常。

我发现的解决方法是添加一个拦截器进行改造,它将检查解码为字符串 body 的最后一个符号是否与有效的 json 响应匹配,如果匹配则添加它们错过了。

interface ApiServiceInterface {

    companion object Factory{

        fun create(): ApiServiceInterface {
            val interceptor = HttpLoggingInterceptor()
            interceptor.level = HttpLoggingInterceptor.Level.BODY
            
            val stringInterceptor = Interceptor { chain: Interceptor.Chain ->
                val request = chain.request()
                val response = chain.proceed(request)
                val source = response.body()?.source()
                source?.request(Long.MAX_VALUE)
                val buffer = source?.buffer()
                var responseString = buffer?.clone()?.readString(Charset.forName("UTF-8"))
                if (responseString != null && responseString.length > 2) {
                    val lastTwo = responseString.takeLast(2)
                    if (lastTwo != "}}") {
                        val lastOne = responseString.takeLast(1)
                        responseString = if (lastOne != "}") {
                            "$responseString}}"
                        } else {
                            "$responseString}"
                        }
                    }
                }
                val contentType = response.body()?.contentType()
                val body = ResponseBody.create(contentType, responseString ?: "")
                return@Interceptor response.newBuilder().body(body).build()
            }

            val client = OkHttpClient.Builder()
                .connectTimeout(30, TimeUnit.SECONDS)
                .writeTimeout(30, TimeUnit.SECONDS)
                .readTimeout(30,TimeUnit.SECONDS)
                .addInterceptor(interceptor)
                .addInterceptor(stringInterceptor)
                .retryOnConnectionFailure(true)
                .connectionPool(ConnectionPool(0, 5, TimeUnit.MINUTES))
                .protocols(listOf(Protocol.HTTP_1_1))
                .build()
            val gson = GsonBuilder().create()
            val retrofit = Retrofit.Builder()
                .addCallAdapterFactory(CoroutineCallAdapterFactory())
                .addConverterFactory(GsonConverterFactory.create(gson))
                .addConverterFactory(ScalarsConverterFactory.create())
                .baseUrl("http://3.124.6.203:5000")
                .client(client)
                .build()
            return retrofit.create(ApiServiceInterface::class.java)
        }
    }


    @Headers("Content-type: application/json", "Connection: close", "Accept-Encoding: identity")
    @POST("/")
    fun requestAsync(@Body data: JsonObject): Deferred<Response>
}

修改后问题没有出现。