改造 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>
}
到目前为止,我发现了以下内容:
- 这个问题只发生在我使用 Android Studio 模拟器 运行 来自 Windows 系列 OS (7, 10, 11) - 这是在 2来自不同网络的不同笔记本电脑。
- 如果 运行 Android Studio 模拟器在 OS X 下,问题将不会在 100% 的情况下重现。
- ARC/Postman 客户端在完成对我后端的相同请求时从来没有任何问题。
- 在 Windows Android Studio 模拟器的 运行 上,此问题在大约 10-50% 的请求中重现,其他请求可以正常工作。
- 相同的请求可能会导致此错误或成功完成。
- 需要大约 11 秒才能完成的响应可能会导致成功,而需要大约 100 毫秒才能完成的响应可能会导致此错误。
- 从改造配置中注释掉
.client(client)
消除了这个问题,但我失去了使用拦截器和其他 OkHttp 功能的机会。
- 添加headers(连接:关闭,Accept-Encoding:身份)不能解决问题。
- 打开或关闭
retryOnConnectionFailure
对问题也没有影响。
- 更改 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`);
});
因此,基于 1
、2
、3
- 这不太可能 server-side 问题。
基于 4
、5
、6
- 这不是格式错误的请求相关或执行时间相关的问题。
来自 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>
}
修改后问题没有出现。
我在将 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>
}
到目前为止,我发现了以下内容:
- 这个问题只发生在我使用 Android Studio 模拟器 运行 来自 Windows 系列 OS (7, 10, 11) - 这是在 2来自不同网络的不同笔记本电脑。
- 如果 运行 Android Studio 模拟器在 OS X 下,问题将不会在 100% 的情况下重现。
- ARC/Postman 客户端在完成对我后端的相同请求时从来没有任何问题。
- 在 Windows Android Studio 模拟器的 运行 上,此问题在大约 10-50% 的请求中重现,其他请求可以正常工作。
- 相同的请求可能会导致此错误或成功完成。
- 需要大约 11 秒才能完成的响应可能会导致成功,而需要大约 100 毫秒才能完成的响应可能会导致此错误。
- 从改造配置中注释掉
.client(client)
消除了这个问题,但我失去了使用拦截器和其他 OkHttp 功能的机会。 - 添加headers(连接:关闭,Accept-Encoding:身份)不能解决问题。
- 打开或关闭
retryOnConnectionFailure
对问题也没有影响。 - 更改 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`);
});
因此,基于 1
、2
、3
- 这不太可能 server-side 问题。
基于 4
、5
、6
- 这不是格式错误的请求相关或执行时间相关的问题。
来自 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% 中它读取所有字符。这是截图:
查看缓冲区本身
782 索引处的字符(按顺序为 783)必须是关闭 json 根的第二个大括号,但取而代之的是第一次迭代开始时的剩余部分。这导致上述异常。
现在,如果我们查看请求成功完成的情况:
缓冲区本身是:
在这种情况下请求会成功。
相同的响应以 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>
}
修改后问题没有出现。