Shorenet for developers

A response before the upload ends

HTTP/2 lets a server answer a request before it has read all of it, and then ask the client to stop sending. Here is what that looks like on the wire, a server you can try it against, and a capture to check your parser with offline.

What the standard allows

RFC 9113, section 8.1: when a response does not depend on the rest of the request, a server can send the whole response before the client has finished sending. It can then send RST_STREAM with the error code NO_ERROR, which asks the client to stop sending without treating anything as an error.

The standard is explicit about the client’s side: “Clients MUST NOT discard responses as a result of receiving such a RST_STREAM”.

So on one stream you see the request’s headers and some of its body, then the complete response, then the reset. Pieces of the upload that were already on their way can still arrive after the reset.

Why it catches parsers out

Usually the request has finished before the response starts, and code written for that order can come to depend on it. A parser that waits for the request to end before it reads the response never reads this one, because the response arrives before the request has ended. A parser that reads every RST_STREAM as a failure reports a complete response as broken. Neither mistake is obvious in a log.

Test it live

Sea trials serves this case at h2.lab.shorenet.co.uk/f/h2-early-response. It answers a POST at once, with a 200 and the text answered before the upload ended, without reading the upload. If the upload is still going, the stream then ends with RST_STREAM(NO_ERROR).

The easiest way to see it is in a browser: h2.lab.shorenet.co.uk/t/h2-early-response streams an upload in eight pieces of 16 KiB, half a second apart, and gets its answer while the rest is still being sent.

From a terminal, send an upload large enough to still be going when the answer comes back, and keep the TLS keys so you can read your capture afterwards:

head -c 33554432 /dev/zero > big.bin
SSLKEYLOGFILE=keys.txt curl --http2 --data-binary @big.bin https://h2.lab.shorenet.co.uk/f/h2-early-response

Record the traffic while it runs, and give your packet analyser keys.txt as its TLS key log. The servers are rate limited and may be slow or offline; Sea trials lists the limits and the other cases.

Reproduce it offline

A live server shows you what happens today. A capture lets you check the same case again, offline, and get the same answer every time. The Chart locker has a synthetic capture of this exchange: cleartext HTTP/2 on port 80, so there are no keys to load. Its server answers with a 413 rather than a 200, and two more pieces of the upload follow the reset.

A correct reading finds these seven frames on stream 1, in this order:

FromFrameCarries
clientHEADERSPOST /upload
clientDATAfirst-chunk-of-the-upload
serverHEADERS:status 413
serverDATA, END_STREAMpayload too large
serverRST_STREAMNO_ERROR
clientDATAsecond-chunk-sent-after-the-response
clientDATA, END_STREAMtail

Your parser passes if it keeps the whole 413 response and every piece of the upload, including the two that arrive after the reset. Download h2-early-response.pcapng and its expected output, h2-early-response.expected.json. The Chart locker lists each file’s SHA-256 and describes the expected-output format field by field.

Test live, reproduce offline

That pairing runs through this site. Sea trials gives you servers that behave in unusual ways on purpose, to test against live. The Chart locker gives you captures with the expected output written down, to reproduce a case offline and check your fix against.