Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
maximilianproell opened this issue Apr 30, 2024 · 6 comments
Labels
bug Bug in existing code

Comments

@maximilianproell
Copy link

maximilianproell commented Apr 30, 2024

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 maximilianproell added the bug Bug in existing code label Apr 30, 2024
@swankjesse
Copy link
Collaborator

swankjesse commented Apr 30, 2024

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.

@maximilianproell
Copy link
Author

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?

@Endeavour233
Copy link
Contributor

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

@maximilianproell
Copy link
Author

maximilianproell commented May 14, 2024

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.

@Endeavour233
Copy link
Contributor

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

@jonapoul
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Bug in existing code
Projects
None yet
Development

No branches or pull requests

4 participants