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