Skip to content

Commit e46a200

Browse files
yschimkeswankjesse
andauthored
Read the response even if writing the request fails (#6295) (#7453)
Closes: 1001 (cherry picked from commit 9533117) Co-authored-by: Jesse Wilson <jesse@swank.ca>
1 parent cf088f8 commit e46a200

File tree

10 files changed

+403
-86
lines changed

10 files changed

+403
-86
lines changed

mockwebserver/src/main/kotlin/okhttp3/mockwebserver/MockResponse.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ class MockResponse : Cloneable {
6060
/**
6161
* Sets the [HTTP/2 error code](https://tools.ietf.org/html/rfc7540#section-7) to be
6262
* returned when resetting the stream.
63-
* This is only valid with [SocketPolicy.RESET_STREAM_AT_START].
63+
* This is only valid with [SocketPolicy.RESET_STREAM_AT_START] and
64+
* [SocketPolicy.DO_NOT_READ_REQUEST_BODY].
6465
*/
6566
@set:JvmName("http2ErrorCode")
6667
var http2ErrorCode = -1

mockwebserver/src/main/kotlin/okhttp3/mockwebserver/MockWebServer.kt

+17-7
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import okhttp3.mockwebserver.SocketPolicy.DISCONNECT_AT_END
7272
import okhttp3.mockwebserver.SocketPolicy.DISCONNECT_AT_START
7373
import okhttp3.mockwebserver.SocketPolicy.DISCONNECT_DURING_REQUEST_BODY
7474
import okhttp3.mockwebserver.SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY
75+
import okhttp3.mockwebserver.SocketPolicy.DO_NOT_READ_REQUEST_BODY
7576
import okhttp3.mockwebserver.SocketPolicy.EXPECT_CONTINUE
7677
import okhttp3.mockwebserver.SocketPolicy.FAIL_HANDSHAKE
7778
import okhttp3.mockwebserver.SocketPolicy.NO_RESPONSE
@@ -627,7 +628,7 @@ class MockWebServer : ExternalResource(), Closeable {
627628

628629
// See warnings associated with these socket policies in SocketPolicy.
629630
when (response.socketPolicy) {
630-
DISCONNECT_AT_END -> {
631+
DISCONNECT_AT_END, DO_NOT_READ_REQUEST_BODY -> {
631632
socket.close()
632633
return false
633634
}
@@ -719,7 +720,9 @@ class MockWebServer : ExternalResource(), Closeable {
719720

720721
var hasBody = false
721722
val policy = dispatcher.peek()
722-
if (contentLength != -1L) {
723+
if (policy.socketPolicy == DO_NOT_READ_REQUEST_BODY) {
724+
// Ignore the body completely.
725+
} else if (contentLength != -1L) {
723726
hasBody = contentLength > 0L
724727
throttledTransfer(policy, socket, source, requestBody.buffer(), contentLength, true)
725728
} else if (chunked) {
@@ -966,7 +969,8 @@ class MockWebServer : ExternalResource(), Closeable {
966969

967970
val response: MockResponse = dispatcher.dispatch(request)
968971

969-
if (response.socketPolicy === DISCONNECT_AFTER_REQUEST) {
972+
val socketPolicy = response.socketPolicy
973+
if (socketPolicy === DISCONNECT_AFTER_REQUEST) {
970974
socket.close()
971975
return
972976
}
@@ -977,9 +981,15 @@ class MockWebServer : ExternalResource(), Closeable {
977981
"and responded: $response protocol is $protocol")
978982
}
979983

980-
if (response.socketPolicy === DISCONNECT_AT_END) {
981-
val connection = stream.connection
982-
connection.shutdown(ErrorCode.NO_ERROR)
984+
when (socketPolicy) {
985+
DISCONNECT_AT_END -> {
986+
stream.connection.shutdown(ErrorCode.NO_ERROR)
987+
}
988+
DO_NOT_READ_REQUEST_BODY -> {
989+
stream.close(ErrorCode.fromHttp2(response.http2ErrorCode)!!, null)
990+
}
991+
else -> {
992+
}
983993
}
984994
}
985995

@@ -1019,7 +1029,7 @@ class MockWebServer : ExternalResource(), Closeable {
10191029
val body = Buffer()
10201030
val requestLine = "$method $path HTTP/1.1"
10211031
var exception: IOException? = null
1022-
if (readBody && !peek.isDuplex) {
1032+
if (readBody && !peek.isDuplex && peek.socketPolicy !== DO_NOT_READ_REQUEST_BODY) {
10231033
try {
10241034
val contentLengthString = headers["content-length"]
10251035
val byteCount = contentLengthString?.toLong() ?: Long.MAX_VALUE

mockwebserver/src/main/kotlin/okhttp3/mockwebserver/SocketPolicy.kt

+7
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ enum class SocketPolicy {
7575
/** Close connection after writing half of the response body (if present). */
7676
DISCONNECT_DURING_RESPONSE_BODY,
7777

78+
/**
79+
* Process the response without even attempting to reading the request body. For HTTP/2 this will
80+
* send [MockResponse.getHttp2ErrorCode] after the response body or trailers. For HTTP/1 this will
81+
* close the socket after the response body or trailers.
82+
*/
83+
DO_NOT_READ_REQUEST_BODY,
84+
7885
/** Don't trust the client during the SSL handshake. */
7986
FAIL_HANDSHAKE,
8087

okhttp/src/main/kotlin/okhttp3/internal/connection/Exchange.kt

+6-1
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,14 @@ class Exchange(
4343
internal val finder: ExchangeFinder,
4444
private val codec: ExchangeCodec
4545
) {
46-
/** Returns true if the request body need not complete before the response body starts. */
46+
/** True if the request body need not complete before the response body starts. */
4747
internal var isDuplex: Boolean = false
4848
private set
4949

50+
/** True if there was an exception on the connection to the peer. */
51+
internal var hasFailure: Boolean = false
52+
private set
53+
5054
internal val connection: RealConnection = codec.connection
5155

5256
internal val isCoalescedConnection: Boolean
@@ -163,6 +167,7 @@ class Exchange(
163167
}
164168

165169
private fun trackFailure(e: IOException) {
170+
hasFailure = true
166171
finder.trackFailure(e)
167172
codec.connection.trackFailure(call, e)
168173
}

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

+94-73
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import java.net.ProtocolException
2020
import okhttp3.Interceptor
2121
import okhttp3.Response
2222
import okhttp3.internal.EMPTY_RESPONSE
23+
import okhttp3.internal.http2.ConnectionShutdownException
2324
import okio.buffer
2425

2526
/** This is the last interceptor in the chain. It makes a network call to the server. */
@@ -33,98 +34,118 @@ class CallServerInterceptor(private val forWebSocket: Boolean) : Interceptor {
3334
val requestBody = request.body
3435
val sentRequestMillis = System.currentTimeMillis()
3536

36-
exchange.writeRequestHeaders(request)
37-
3837
var invokeStartEvent = true
3938
var responseBuilder: Response.Builder? = null
40-
if (HttpMethod.permitsRequestBody(request.method) && requestBody != null) {
41-
// If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
42-
// Continue" response before transmitting the request body. If we don't get that, return
43-
// what we did get (such as a 4xx response) without ever transmitting the request body.
44-
if ("100-continue".equals(request.header("Expect"), ignoreCase = true)) {
45-
exchange.flushRequest()
46-
responseBuilder = exchange.readResponseHeaders(expectContinue = true)
47-
exchange.responseHeadersStart()
48-
invokeStartEvent = false
49-
}
50-
if (responseBuilder == null) {
51-
if (requestBody.isDuplex()) {
52-
// Prepare a duplex body so that the application can send a request body later.
39+
var sendRequestException: IOException? = null
40+
try {
41+
exchange.writeRequestHeaders(request)
42+
43+
if (HttpMethod.permitsRequestBody(request.method) && requestBody != null) {
44+
// If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
45+
// Continue" response before transmitting the request body. If we don't get that, return
46+
// what we did get (such as a 4xx response) without ever transmitting the request body.
47+
if ("100-continue".equals(request.header("Expect"), ignoreCase = true)) {
5348
exchange.flushRequest()
54-
val bufferedRequestBody = exchange.createRequestBody(request, true).buffer()
55-
requestBody.writeTo(bufferedRequestBody)
49+
responseBuilder = exchange.readResponseHeaders(expectContinue = true)
50+
exchange.responseHeadersStart()
51+
invokeStartEvent = false
52+
}
53+
if (responseBuilder == null) {
54+
if (requestBody.isDuplex()) {
55+
// Prepare a duplex body so that the application can send a request body later.
56+
exchange.flushRequest()
57+
val bufferedRequestBody = exchange.createRequestBody(request, true).buffer()
58+
requestBody.writeTo(bufferedRequestBody)
59+
} else {
60+
// Write the request body if the "Expect: 100-continue" expectation was met.
61+
val bufferedRequestBody = exchange.createRequestBody(request, false).buffer()
62+
requestBody.writeTo(bufferedRequestBody)
63+
bufferedRequestBody.close()
64+
}
5665
} else {
57-
// Write the request body if the "Expect: 100-continue" expectation was met.
58-
val bufferedRequestBody = exchange.createRequestBody(request, false).buffer()
59-
requestBody.writeTo(bufferedRequestBody)
60-
bufferedRequestBody.close()
66+
exchange.noRequestBody()
67+
if (!exchange.connection.isMultiplexed) {
68+
// If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection
69+
// from being reused. Otherwise we're still obligated to transmit the request body to
70+
// leave the connection in a consistent state.
71+
exchange.noNewExchangesOnConnection()
72+
}
6173
}
6274
} else {
6375
exchange.noRequestBody()
64-
if (!exchange.connection.isMultiplexed) {
65-
// If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection
66-
// from being reused. Otherwise we're still obligated to transmit the request body to
67-
// leave the connection in a consistent state.
68-
exchange.noNewExchangesOnConnection()
69-
}
7076
}
71-
} else {
72-
exchange.noRequestBody()
73-
}
7477

75-
if (requestBody == null || !requestBody.isDuplex()) {
76-
exchange.finishRequest()
77-
}
78-
if (responseBuilder == null) {
79-
responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!
80-
if (invokeStartEvent) {
81-
exchange.responseHeadersStart()
82-
invokeStartEvent = false
78+
if (requestBody == null || !requestBody.isDuplex()) {
79+
exchange.finishRequest()
80+
}
81+
} catch (e: IOException) {
82+
if (e is ConnectionShutdownException) {
83+
throw e // No request was sent so there's no response to read.
84+
}
85+
if (!exchange.hasFailure) {
86+
throw e // Don't attempt to read the response; we failed to send the request.
8387
}
88+
sendRequestException = e
8489
}
85-
var response = responseBuilder
86-
.request(request)
87-
.handshake(exchange.connection.handshake())
88-
.sentRequestAtMillis(sentRequestMillis)
89-
.receivedResponseAtMillis(System.currentTimeMillis())
90-
.build()
91-
var code = response.code
92-
if (code == 100) {
93-
// Server sent a 100-continue even though we did not request one. Try again to read the actual
94-
// response status.
95-
responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!
96-
if (invokeStartEvent) {
97-
exchange.responseHeadersStart()
90+
91+
try {
92+
if (responseBuilder == null) {
93+
responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!
94+
if (invokeStartEvent) {
95+
exchange.responseHeadersStart()
96+
invokeStartEvent = false
97+
}
9898
}
99-
response = responseBuilder
99+
var response = responseBuilder
100100
.request(request)
101101
.handshake(exchange.connection.handshake())
102102
.sentRequestAtMillis(sentRequestMillis)
103103
.receivedResponseAtMillis(System.currentTimeMillis())
104104
.build()
105-
code = response.code
106-
}
105+
var code = response.code
106+
if (code == 100) {
107+
// Server sent a 100-continue even though we did not request one. Try again to read the
108+
// actual response status.
109+
responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!
110+
if (invokeStartEvent) {
111+
exchange.responseHeadersStart()
112+
}
113+
response = responseBuilder
114+
.request(request)
115+
.handshake(exchange.connection.handshake())
116+
.sentRequestAtMillis(sentRequestMillis)
117+
.receivedResponseAtMillis(System.currentTimeMillis())
118+
.build()
119+
code = response.code
120+
}
107121

108-
exchange.responseHeadersEnd(response)
122+
exchange.responseHeadersEnd(response)
109123

110-
response = if (forWebSocket && code == 101) {
111-
// Connection is upgrading, but we need to ensure interceptors see a non-null response body.
112-
response.newBuilder()
113-
.body(EMPTY_RESPONSE)
114-
.build()
115-
} else {
116-
response.newBuilder()
117-
.body(exchange.openResponseBody(response))
118-
.build()
119-
}
120-
if ("close".equals(response.request.header("Connection"), ignoreCase = true) ||
121-
"close".equals(response.header("Connection"), ignoreCase = true)) {
122-
exchange.noNewExchangesOnConnection()
123-
}
124-
if ((code == 204 || code == 205) && response.body?.contentLength() ?: -1L > 0L) {
125-
throw ProtocolException(
126-
"HTTP $code had non-zero Content-Length: ${response.body?.contentLength()}")
124+
response = if (forWebSocket && code == 101) {
125+
// Connection is upgrading, but we need to ensure interceptors see a non-null response body.
126+
response.newBuilder()
127+
.body(EMPTY_RESPONSE)
128+
.build()
129+
} else {
130+
response.newBuilder()
131+
.body(exchange.openResponseBody(response))
132+
.build()
133+
}
134+
if ("close".equals(response.request.header("Connection"), ignoreCase = true) ||
135+
"close".equals(response.header("Connection"), ignoreCase = true)) {
136+
exchange.noNewExchangesOnConnection()
137+
}
138+
if ((code == 204 || code == 205) && response.body?.contentLength() ?: -1L > 0L) {
139+
throw ProtocolException(
140+
"HTTP $code had non-zero Content-Length: ${response.body?.contentLength()}")
141+
}
142+
return response
143+
} catch (e: IOException) {
144+
if (sendRequestException != null) {
145+
sendRequestException.addSuppressed(e)
146+
throw sendRequestException
147+
}
148+
throw e
127149
}
128-
return response
129150
}
130151
}

okhttp/src/main/kotlin/okhttp3/internal/http1/Http1ExchangeCodec.kt

+3-1
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,9 @@ class Http1ExchangeCodec(
170170
}
171171

172172
override fun readResponseHeaders(expectContinue: Boolean): Response.Builder? {
173-
check(state == STATE_OPEN_REQUEST_BODY || state == STATE_READ_RESPONSE_HEADERS) {
173+
check(state == STATE_OPEN_REQUEST_BODY ||
174+
state == STATE_WRITING_REQUEST_BODY ||
175+
state == STATE_READ_RESPONSE_HEADERS) {
174176
"state: $state"
175177
}
176178

okhttp/src/main/kotlin/okhttp3/internal/http2/Http2ExchangeCodec.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ class Http2ExchangeCodec(
9393
}
9494

9595
override fun readResponseHeaders(expectContinue: Boolean): Response.Builder? {
96-
val headers = stream!!.takeHeaders()
96+
val stream = stream ?: throw IOException("stream wasn't created")
97+
val headers = stream.takeHeaders()
9798
val responseBuilder = readHttp2HeadersList(headers, protocol)
9899
return if (expectContinue && responseBuilder.code == HTTP_CONTINUE) {
99100
null

okhttp/src/main/kotlin/okhttp3/internal/http2/Http2Stream.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ class Http2Stream internal constructor(
350350
synchronized(this@Http2Stream) {
351351
readTimeout.enter()
352352
try {
353-
if (errorCode != null) {
353+
if (errorCode != null && !finished) {
354354
// Prepare to deliver an error.
355355
errorExceptionToDeliver = errorException ?: StreamResetException(errorCode!!)
356356
}

okhttp/src/test/java/okhttp3/EventListenerTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -1118,7 +1118,7 @@ private void writeChunk(BufferedSink sink) throws IOException {
11181118
assertThat(listener.recordedEventTypes()).containsExactly(
11191119
"CallStart", "ProxySelectStart", "ProxySelectEnd", "DnsStart", "DnsEnd", "ConnectStart",
11201120
"ConnectEnd", "ConnectionAcquired", "RequestHeadersStart", "RequestHeadersEnd",
1121-
"RequestBodyStart", "RequestFailed", "ConnectionReleased", "CallFailed");
1121+
"RequestBodyStart", "RequestFailed", "ResponseFailed", "ConnectionReleased", "CallFailed");
11221122
}
11231123

11241124
@Test public void requestBodySuccessHttp1OverHttps() throws IOException {

0 commit comments

Comments
 (0)