Skip to content

Closing a partial fixed-length duplex request body does not complete the request side #9648

Description

@osipxd

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:

  1. Start an HTTP/2 duplex request whose body returns a positive contentLength().
  2. Write only part of the request body and receive the server's final response.
  3. Cancel the call and close the partially written request sink.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions