The end of a body

A body ends where the protocol says it ends, and never at a zero-sized transfer. Reading and writing are not symmetric here: the end of an incoming body is reported, the end of an outgoing one is stated.

Reading

Reader::read_some() completes with errors::eof and zero bytes at the end of the body, and keeps reporting that for every read after it: there is no state in which a finished body starts looking unfinished again. A body cut short completes with errors::partial_message instead. The two are the whole difference between “the peer is done” and “the peer is gone”, and neither degrades into the other.

Reading into a zero-length buffer completes immediately, with success, and says nothing at all about the body. It is not a way to poll for the end.

Writing

Writer::write_eof() ends the body, with or without a buffer of its own, see Programmer’s guide. write({}) is a no-op: it succeeds and leaves the body open.

That an empty write means nothing is deliberate. Generic code that forwards whatever it just read will hand over an empty buffer eventually, and a message that ends by accident is a bug that only shows up under load. Ending a body is an explicit operation.

After the end

All three protocols answer in the same order, whatever the state of the connection underneath:

  1. An empty non-EOF write succeeds, always, even after the body has ended.

  2. Once the body has ended, writing data fails with errc::broken_pipe, whether it comes through write() or write_eof(buffer). Ending an already ended body without data succeeds: it is idempotent.

  3. Only then do the errors of a closed or cancelled stream apply. Their codes differ per protocol: connection_aborted for HTTP/1.1 and HTTP/2, connection_reset for HTTP/3.

This order survives the session going away. A detached reader or writer answers from the state it latched, so a read past a detached stream still distinguishes a clean end (eof) from a truncated one (partial_message), without touching the connection that is gone.

Cancellation

Cancelling write_eof() takes the end of the body back if the protocol stack has not been given it yet: the body is open again, and what the write has not taken can follow. Once the stack has it, the end stands, as no protocol can un-send a FIN, and issuing write_eof() again is the idempotent re-end of rule 2 – or, over HTTP/3, adopts the end that is still on its way. Which of the two a cancelled write_eof() leaves depends on how far it had come, so going on with write_eof(buffer + n), where n is what it has taken, is right either way. See Cancellation.