【问题标题】:Retrofit OkHttp - "unexpected end of stream"改造 OkHttp - “流的意外结束”
【发布时间】:2022-03-28 02:38:38
【问题描述】:

在使用带有 OkHttp3 (4.9.1) 的 Retrofit (2.9.0) 时出现“意外的流结束”

改造配置:

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

服务器端配置:

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`);
});

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

我已经阅读了几乎一半的 stackoverflow 是搜索分辨率,例如:
unexpected end of stream retrofit
Retrofit OkHttp unexpected end of stream on Connection error
并在 Github 上的 OkHttp 进行讨论:
https://github.com/square/okhttp/issues/3682
https://github.com/square/okhttp/issues/3715
但到目前为止没有任何帮助。

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

更新

我有更多有关情况的信息。

首先,我将后端的标头更改为不传递Content-Length,而是传递Transfer-Encoding : identity。我不知道为什么,但是如果这些标题都存在,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$1.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 数组缓冲区。

这个缓冲区本身是在同一模块的fillBuffer 方法中接收的,它的长度限制为 1024 个元素。在我的情况下,后端响应比这个值(1807 个字符)长,所以当JsonReader.java 将我的响应解析为 json 对象时,它会在 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 类上从 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,它构成了响应对象的一部分。

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

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

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

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

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

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

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

Postman结尾的相同响应: Postman 解析响应的成功率为 100%,对于 OS X 托管的 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

发现这个未解决的问题是完全相同的行为:
Retrofit Json data truncated
还来自 Android Studio 模拟器问题跟踪器的评论:
https://issuetracker.google.com/issues/119027639#comment9

【问题讨论】:

    标签: kotlin retrofit okhttp


    【解决方案1】:

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

    当 Android Studio 的模拟器在 Windows 系列操作系统中运行(检查 7 和 10)从服务器接收到 json 类型的回复并进行改造时,当它被解码为字符串时,它可以以各种概率松散主体的 1 或 2 个最后一个符号,这符号包含右大括号,因此 gson 转换器无法将此类主体解析为对象,从而导致抛出异常。

    我发现的解决方法的想法是添加一个拦截器来改造,如果它的最后一个符号与有效的 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>
    }
    

    在此更改后,问题没有发生。

    【讨论】:

      猜你喜欢
      • 2019-09-20
      • 2020-08-17
      • 1970-01-01
      • 2017-11-12
      • 2020-01-23
      • 2022-08-24
      • 2019-01-20
      • 1970-01-01
      • 2015-11-17
      相关资源
      最近更新 更多