Skip to content

Commit 8579689

Browse files
Don't force a response body read on all trailers (#8904)
We only need to read the response body if its coming from a real server. Closes: #8902 Co-authored-by: Jesse Wilson <[email protected]>
1 parent 55a2c44 commit 8579689

File tree

3 files changed

+78
-11
lines changed

3 files changed

+78
-11
lines changed

okhttp/src/commonJvmAndroid/kotlin/okhttp3/Response.kt

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import okhttp3.internal.connection.Exchange
2828
import okhttp3.internal.http.HTTP_PERM_REDIRECT
2929
import okhttp3.internal.http.HTTP_TEMP_REDIRECT
3030
import okhttp3.internal.http.parseChallenges
31-
import okhttp3.internal.skipAll
3231
import okio.Buffer
3332

3433
/**
@@ -191,13 +190,7 @@ class Response internal constructor(
191190
* dropped.
192191
*/
193192
@Throws(IOException::class)
194-
fun trailers(): Headers {
195-
val source = body.source()
196-
if (source.isOpen) {
197-
source.skipAll()
198-
}
199-
return trailersSource.get()
200-
}
193+
fun trailers(): Headers = trailersSource.get()
201194

202195
/**
203196
* Peeks up to [byteCount] bytes from the response body and returns them as a new response
@@ -478,7 +471,6 @@ class Response internal constructor(
478471

479472
internal fun initExchange(exchange: Exchange) {
480473
this.exchange = exchange
481-
this.trailersSource = TrailersSource { exchange.trailers() }
482474
}
483475

484476
open fun build(): Response {

okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/CallServerInterceptor.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import okhttp3.Interceptor
2121
import okhttp3.Response
2222
import okhttp3.internal.connection.Exchange
2323
import okhttp3.internal.http2.ConnectionShutdownException
24+
import okhttp3.internal.skipAll
2425
import okhttp3.internal.stripBody
2526
import okio.buffer
2627

@@ -129,10 +130,17 @@ class CallServerInterceptor(
129130
// Connection is upgrading, but we need to ensure interceptors see a non-null response body.
130131
response.stripBody()
131132
} else {
133+
val responseBody = exchange.openResponseBody(response)
132134
response
133135
.newBuilder()
134-
.body(exchange.openResponseBody(response))
135-
.build()
136+
.body(responseBody)
137+
.trailers {
138+
val source = responseBody.source()
139+
if (source.isOpen) {
140+
source.skipAll()
141+
}
142+
exchange.trailers()
143+
}.build()
136144
}
137145
if ("close".equals(response.request.header("Connection"), ignoreCase = true) ||
138146
"close".equals(response.header("Connection"), ignoreCase = true)

okhttp/src/jvmTest/kotlin/okhttp3/TrailersTest.kt

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ import mockwebserver3.SocketEffect.CloseSocket
3535
import mockwebserver3.SocketEffect.ShutdownConnection
3636
import mockwebserver3.junit5.StartStop
3737
import okhttp3.Headers.Companion.headersOf
38+
import okhttp3.HttpUrl.Companion.toHttpUrl
39+
import okhttp3.ResponseBody.Companion.toResponseBody
3840
import okhttp3.testing.PlatformRule
41+
import okio.BufferedSource
3942
import okio.IOException
4043
import okio.Path.Companion.toPath
4144
import okio.fakefilesystem.FakeFileSystem
@@ -419,6 +422,70 @@ open class TrailersTest {
419422
}
420423
}
421424

425+
@Test
426+
fun bufferResponseBodyAndReadTrailersHttp1() {
427+
bufferResponseBodyAndReadTrailers(Protocol.HTTP_1_1)
428+
}
429+
430+
@Test
431+
fun bufferResponseBodyAndReadTrailersHttp2() {
432+
bufferResponseBodyAndReadTrailers(Protocol.HTTP_2)
433+
}
434+
435+
private fun bufferResponseBodyAndReadTrailers(protocol: Protocol) {
436+
enableProtocol(protocol)
437+
438+
server.enqueue(
439+
MockResponse
440+
.Builder()
441+
.trailers(headersOf("t1", "v1"))
442+
.body(protocol, "Hello")
443+
.build(),
444+
)
445+
446+
val call = client.newCall(Request(server.url("/")))
447+
call.execute().use { originalResponse ->
448+
val responseBodyData = originalResponse.body.byteString()
449+
val responseTrailers = originalResponse.trailers()
450+
assertThat(responseTrailers).isEqualTo(headersOf("t1", "v1"))
451+
452+
val rewrittenResponse =
453+
originalResponse
454+
.newBuilder()
455+
.body(responseBodyData.toResponseBody())
456+
.build()
457+
assertThat(rewrittenResponse.body.string()).isEqualTo("Hello")
458+
assertThat(rewrittenResponse.trailers()).isEqualTo(headersOf("t1", "v1"))
459+
}
460+
}
461+
462+
/**
463+
* We had a bug where a custom `ResponseBody` interacted poorly with `Response.trailers()`.
464+
* Confirm custom trailers can be read without even accessing the response body.
465+
*/
466+
@Test
467+
fun customTrailersDoNotUseResponseBody() {
468+
val response =
469+
Response
470+
.Builder()
471+
.request(Request(url = "https://example.com".toHttpUrl()))
472+
.protocol(Protocol.HTTP_1_1)
473+
.code(200)
474+
.message("OK")
475+
.body(
476+
object : ResponseBody() {
477+
override fun contentType(): MediaType? = null
478+
479+
override fun contentLength(): Long = -1L
480+
481+
override fun source(): BufferedSource = error("unexpected call")
482+
},
483+
).trailers { headersOf("t1", "v1") }
484+
.build()
485+
486+
assertThat(response.trailers()).isEqualTo(headersOf("t1", "v1"))
487+
}
488+
422489
private fun MockResponse.Builder.body(
423490
protocol: Protocol,
424491
body: String,

0 commit comments

Comments
 (0)