This was observed with OkHttp 5.3.2 while working on ktorio/ktor#5791. The current OkHttp main branch contains the same relevant implementation.
When a fixed-length duplex request is cancelled before all declared bytes are written, closing its sink throws:
ProtocolException: unexpected end of stream
The exception is expected, but the request side is not marked complete, leaving the connection allocated:
connectionCount() == 1
idleConnectionCount() == 0
How to reproduce
The observed sequence is:
- Start an HTTP/2 duplex request whose body returns a positive
contentLength().
- Write only part of the request body and receive the server's final response.
- Cancel the call and close the partially written request sink.
- Observe the length-mismatch exception and that the connection does not become idle.
Exchange.RequestBodySink.close()throws before calling complete(...):
|
override fun close() { |
|
if (closed) return |
|
closed = true |
|
if (contentLength != -1L && bytesReceived != contentLength) { |
|
throw ProtocolException("unexpected end of stream") |
|
} |
|
try { |
|
super.close() |
|
complete(null) |
|
} catch (e: IOException) { |
|
throw complete(e)!! |
|
} |
|
} |
A subsequent close() cannot complete the request because closed is already true.
Should the request side be completed with failure before reporting the length mismatch, so the cancelled call can release its connection allocation?
Workaround
As a workaround in Ktor, after request cancellation we attempt to call flush() before the sink is closed. Because the HTTP/2 stream has already been reset, RequestBodySink.flush() fails and invokes complete(e), which marks the request side complete.
Unfortunately this relies on OkHttp’s internal implementation and only works when flush() fails after cancellation.
This was observed with OkHttp 5.3.2 while working on ktorio/ktor#5791. The current OkHttp
mainbranch contains the same relevant implementation.When a fixed-length duplex request is cancelled before all declared bytes are written, closing its sink throws:
The exception is expected, but the request side is not marked complete, leaving the connection allocated:
How to reproduce
The observed sequence is:
contentLength().Exchange.RequestBodySink.close()throws before callingcomplete(...):okhttp/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/Exchange.kt
Lines 294 to 306 in 61423f4
A subsequent
close()cannot complete the request becauseclosedis alreadytrue.Should the request side be completed with failure before reporting the length mismatch, so the cancelled call can release its connection allocation?
Workaround
As a workaround in Ktor, after request cancellation we attempt to call
flush()before the sink is closed. Because the HTTP/2 stream has already been reset,RequestBodySink.flush()fails and invokescomplete(e), which marks the request side complete.Unfortunately this relies on OkHttp’s internal implementation and only works when
flush()fails after cancellation.