Calling response.body.string() on a MockWebServer response throws a SocketTimeoutException
I have an Android Instrumentation test where I simply call my API and return a MockResponse, something like:
val mockResponse = MockResponse()
.setResponseCode(200)
.setBody(
"{ SomeBodyContent }"
)
.setHeaders(
Headers.headersOf(
"Set-Cookie",
"sessionId=someId; path=/; samesite=None;"
)
)
I use a custom dispatcher to return the Response:
val dispatcher: Dispatcher = object : Dispatcher() {
@Throws(InterruptedException::class)
override fun dispatch(request: RecordedRequest): MockResponse {
when (request.path) {
"/api/endpoint" -> return response
}
return MockResponse().setResponseCode(404)
}
}
The mockWebServer is set up as follows:
val mockWebServer: MockWebServer = MockWebServer()
mockWebServer.start(8080)
mockWebServer.dispatcher = dispatcher
Then I call the API using RxJava
val result = apiService
.doSomeRequest(TheRequest())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.blockingFirst()
Now, I have an interceptor which causes the issue:
client.addInterceptor { chain ->
val response = chain.proceed(chain.request())
var bodyString: String? = null
response.body?.let {
bodyString = it.string() // Throws a SocketTimeoutException
}
}
Why is the SocketTimeoutException thrown? With the real server, the interceptor works and there is no issue calling the response.body.string() function. When I remove this interceptor, the test succeeds and the body is also correctly parsed with Jackson. Is there a bug in the MockWebServer implementation?
Interceptors must not consume the response body. This is forbidden because it prevents the original caller from consuming the streamed response body. You can work-around this by using peekBody(5L) instead.
Use whatever value you want other than 5 for the number of bytes to peek. You can use 100L * 1024 * 1024 if you want to peek up to 100 MiB for example.
Interceptors must not consume the response body. This is forbidden because it prevents the original caller from consuming the streamed response body. You can work-around this by using
peekBody(5L)instead.Use whatever value you want other than 5 for the number of bytes to peek. You can use
100L * 1024 * 1024if you want to peek up to 100 MiB for example.
Thank you for the response. However, using bodyString = response.peekBody(100L * 1024 * 1024).string() still throws a SocketTimeoutException. Is there something I'm missing in the setup?
Interceptors must not consume the response body. This is forbidden because it prevents the original caller from consuming the streamed response body. You can work-around this by using
peekBody(5L)instead. Use whatever value you want other than 5 for the number of bytes to peek. You can use100L * 1024 * 1024if you want to peek up to 100 MiB for example.Thank you for the response. However, using
bodyString = response.peekBody(100L * 1024 * 1024).string()still throws a SocketTimeoutException. Is there something I'm missing in the setup?
I wrote a simple test but can't reproduce your issue (no SocketTimeoutException).
fun bodyString() {
server.enqueue(
MockResponse().setResponseCode(200).setBody("{ body content }")
)
var bodystring: String? = null
client =
OkHttpClient.Builder()
.addInterceptor(
Interceptor { chain ->
val response = chain.proceed(chain.request())
response.body?.let {
bodystring = it.string()
}
response
},
)
.build()
val builder = Request.Builder()
builder.url(server.url("/"))
val call = client.newCall(builder.build())
call.execute()
assertEquals("{ body content }", bodystring)
}
Interceptors must not consume the response body. This is forbidden because it prevents the original caller from consuming the streamed response body. You can work-around this by using
peekBody(5L)instead. Use whatever value you want other than 5 for the number of bytes to peek. You can use100L * 1024 * 1024if you want to peek up to 100 MiB for example.Thank you for the response. However, using
bodyString = response.peekBody(100L * 1024 * 1024).string()still throws a SocketTimeoutException. Is there something I'm missing in the setup?I wrote a simple test but can't reproduce your issue (no SocketTimeoutException).
fun bodyString() { server.enqueue( MockResponse().setResponseCode(200).setBody("{ body content }") ) var bodystring: String? = null client = OkHttpClient.Builder() .addInterceptor( Interceptor { chain -> val response = chain.proceed(chain.request()) response.body?.let { bodystring = it.string() } response }, ) .build() val builder = Request.Builder() builder.url(server.url("/")) val call = client.newCall(builder.build()) call.execute() assertEquals("{ body content }", bodystring) }
I now finally found out what causes the SocketTimeoutException. In my example, I add the headers to the MockResponse. So even your simple test crashes with the SocketTimeoutException right at the body.string() line in the interceptor if you use:
MockResponse().setResponseCode(200).setBody("{ body content }").setHeaders(
Headers.headersOf(
"Set-Cookie",
"sessionId=someId; path=/; samesite=None;"
)
)
The stacktrace is:
java.net.SocketTimeoutException: timeout
at okio.SocketAsyncTimeout.newTimeoutException(JvmOkio.kt:146)
at okio.AsyncTimeout.access$newTimeoutException(AsyncTimeout.kt:161)
at okio.AsyncTimeout$source$1.read(AsyncTimeout.kt:339)
at okio.RealBufferedSource.read(RealBufferedSource.kt:192)
at okhttp3.internal.http1.Http1ExchangeCodec$AbstractSource.read(Http1ExchangeCodec.kt:339)
at okhttp3.internal.http1.Http1ExchangeCodec$UnknownLengthSource.read(Http1ExchangeCodec.kt:475)
at okhttp3.internal.connection.Exchange$ResponseBodySource.read(Exchange.kt:281)
at okio.Buffer.writeAll(Buffer.kt:1303)
at okio.RealBufferedSource.readString(RealBufferedSource.kt:96)
at okhttp3.ResponseBody.string(ResponseBody.kt:187)
at com.test.mockwebservertestapp.ExampleInstrumentedTest.useAppContext$lambda$1(ExampleInstrumentedTest.kt:76)
at com.test.mockwebservertestapp.ExampleInstrumentedTest.$r8$lambda$IPYPGjegXlZus8n5eu1ealS6yag(Unknown Source:0)
at com.test.mockwebservertestapp.ExampleInstrumentedTest$$ExternalSyntheticLambda0.intercept(D8$$SyntheticClass:0)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109)
at okhttp3.internal.connection.RealCall.getResponseWithInterceptorChain$okhttp(RealCall.kt:201)
at okhttp3.internal.connection.RealCall.execute(RealCall.kt:154)
at com.test.mockwebservertestapp.ExampleInstrumentedTest.useAppContext(ExampleInstrumentedTest.kt:86)
... 32 trimmed
Caused by: java.net.SocketException: Socket closed
at java.net.SocketInputStream.read(SocketInputStream.java:188)
at java.net.SocketInputStream.read(SocketInputStream.java:143)
at okio.InputStreamSource.read(JvmOkio.kt:93)
at okio.AsyncTimeout$source$1.read(AsyncTimeout.kt:128)
... 47 more
If I remove the setHeaders function, the test runs just fine.
Also, one important point I forgot to mention: It's an Android Instrumentation test. I edited the issue.
Interceptors must not consume the response body. This is forbidden because it prevents the original caller from consuming the streamed response body. You can work-around this by using
peekBody(5L)instead. Use whatever value you want other than 5 for the number of bytes to peek. You can use100L * 1024 * 1024if you want to peek up to 100 MiB for example.Thank you for the response. However, using
bodyString = response.peekBody(100L * 1024 * 1024).string()still throws a SocketTimeoutException. Is there something I'm missing in the setup?I wrote a simple test but can't reproduce your issue (no SocketTimeoutException).
fun bodyString() { server.enqueue( MockResponse().setResponseCode(200).setBody("{ body content }") ) var bodystring: String? = null client = OkHttpClient.Builder() .addInterceptor( Interceptor { chain -> val response = chain.proceed(chain.request()) response.body?.let { bodystring = it.string() } response }, ) .build() val builder = Request.Builder() builder.url(server.url("/")) val call = client.newCall(builder.build()) call.execute() assertEquals("{ body content }", bodystring) }I now finally found out what causes the SocketTimeoutException. In my example, I add the headers to the MockResponse. So even your simple test crashes with the SocketTimeoutException right at the
body.string()line in the interceptor if you use:MockResponse().setResponseCode(200).setBody("{ body content }").setHeaders( Headers.headersOf( "Set-Cookie", "sessionId=someId; path=/; samesite=None;" ) )The stacktrace is:
java.net.SocketTimeoutException: timeout at okio.SocketAsyncTimeout.newTimeoutException(JvmOkio.kt:146) at okio.AsyncTimeout.access$newTimeoutException(AsyncTimeout.kt:161) at okio.AsyncTimeout$source$1.read(AsyncTimeout.kt:339) at okio.RealBufferedSource.read(RealBufferedSource.kt:192) at okhttp3.internal.http1.Http1ExchangeCodec$AbstractSource.read(Http1ExchangeCodec.kt:339) at okhttp3.internal.http1.Http1ExchangeCodec$UnknownLengthSource.read(Http1ExchangeCodec.kt:475) at okhttp3.internal.connection.Exchange$ResponseBodySource.read(Exchange.kt:281) at okio.Buffer.writeAll(Buffer.kt:1303) at okio.RealBufferedSource.readString(RealBufferedSource.kt:96) at okhttp3.ResponseBody.string(ResponseBody.kt:187) at com.test.mockwebservertestapp.ExampleInstrumentedTest.useAppContext$lambda$1(ExampleInstrumentedTest.kt:76) at com.test.mockwebservertestapp.ExampleInstrumentedTest.$r8$lambda$IPYPGjegXlZus8n5eu1ealS6yag(Unknown Source:0) at com.test.mockwebservertestapp.ExampleInstrumentedTest$$ExternalSyntheticLambda0.intercept(D8$$SyntheticClass:0) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) at okhttp3.internal.connection.RealCall.getResponseWithInterceptorChain$okhttp(RealCall.kt:201) at okhttp3.internal.connection.RealCall.execute(RealCall.kt:154) at com.test.mockwebservertestapp.ExampleInstrumentedTest.useAppContext(ExampleInstrumentedTest.kt:86) ... 32 trimmed Caused by: java.net.SocketException: Socket closed at java.net.SocketInputStream.read(SocketInputStream.java:188) at java.net.SocketInputStream.read(SocketInputStream.java:143) at okio.InputStreamSource.read(JvmOkio.kt:93) at okio.AsyncTimeout$source$1.read(AsyncTimeout.kt:128) ... 47 moreIf I remove the
setHeadersfunction, the test runs just fine. Also, one important point I forgot to mention: It's an Android Instrumentation test. I edited the issue.
I think the issue raises because the content-length header, which is set when we setBody, is missing in response due to setHeaders overwriting all the headers set previously. Without content-length, along with the fact that our mockresponse is not able to notify the reader that it has reached the end of file(EOF)(not sure about this, just a personal guess, need help from @swankjesse),the reader will keep on waiting until timeout.
After swapping the order of setbody and setheaders, my simple test passed.
server.enqueue(
MockResponse().setResponseCode(200).setHeaders(
Headers.headersOf(
"Set-Cookie",
"sessionId=someId; path=/; samesite=None;"
)
)
.setBody("{ body content }"))
Just had the same issue - fixed by either:
- moving the
setBodycall aftersetHeaders, or - manually setting the
Content-Lengthheader value to the length of your JSON string.