okhttp icon indicating copy to clipboard operation
okhttp copied to clipboard

Calling response.body.string() on a MockWebServer response throws a SocketTimeoutException

Open maximilianproell opened this issue 1 year ago • 6 comments

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?

maximilianproell avatar Apr 30 '24 13:04 maximilianproell

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.

swankjesse avatar Apr 30 '24 21:04 swankjesse

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.

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?

maximilianproell avatar May 02 '24 09:05 maximilianproell

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.

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

Endeavour233 avatar May 05 '24 12:05 Endeavour233

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.

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.

maximilianproell avatar May 14 '24 09:05 maximilianproell

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.

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.

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 }"))

Endeavour233 avatar May 14 '24 12:05 Endeavour233

Just had the same issue - fixed by either:

  1. moving the setBody call after setHeaders, or
  2. manually setting the Content-Length header value to the length of your JSON string.

jonapoul avatar Oct 21 '24 09:10 jonapoul