Skip to content

v0.5.0

Latest

Choose a tag to compare

@github-actions github-actions released this 01 Sep 10:43
· 1 commit to master since this release
Six bugs, every one of them a request that never came back.

HTTP/2 retired streams by counting them. A server that answers without reading
the request body ends the stream and then resets it, and that reset lands after
the next request has gone out, so it took the new stream's place; when that
stream did end, nothing was left to notice, and it stayed in flight for the
rest of a run that could no longer finish. A POST to nginx hung about one run
in three.

HTTP/2 sent a request body and a header block whole however large they were.
SETTINGS_MAX_FRAME_SIZE was not read at all, so both could pass the 16 KiB
every peer is allowed to assume: a 20 KB body failed against haproxy, httpd,
envoy, tomcat, hypercorn, h2o, nghttpx, varnish, deno, node and bun. nginx and
caddy accept an oversized frame, which is why this lasted.

An HTTP/2 body had to fit the connection window and the stream's whole or the
request was never started, and nothing started it later - anything over 65535
bytes stopped the run with no error and no end. Bodies now leave as credit
arrives.

Any body over about 64 KiB failed on an https URL, HTTP/1.1 and HTTP/2 alike.
rustls will not take unbounded plaintext before the handshake finishes, and the
first request is handed over while it is still in flight.

QUIC threw away the material for following a key update. Either end may retire
its 1-RTT keys and say so by flipping one bit; a peer that did became a peer
whose every packet was unreadable, while the connection stayed up and the
requests stayed open. The end-to-end tests hung on this about one run in five.

Only ack-eliciting packets are in flight, and this counted every one. A packet
carrying nothing but an ACK never draws one back, so it never left the figure;
answering a large response sends a great many, and once enough had piled up to
fill the congestion window nothing ack-eliciting could go out, so the peer had
no reason to acknowledge anything. HTTP/3 against nginx hung seventeen runs in
twenty.

And two things shb would not do at all. It gave up on a Retry, so a server that
validates addresses was unreachable - none of 100 requests where h2load managed
all of them. It opened every stream a run asked for the moment the socket
connected, a round trip before the peer could say how many it would take, and
httpd, h2o and nghttpx refused 600 of 800.

New: --timeout gives up on a response after a set time, counts the request as
an error and replaces its connection, so a wedged server is a result rather
than a wait. Off by default.

Faster: HTTP/2 collects a batch of completions and writes the connection once
afterwards rather than sending from each, which is 21 % at 32 streams and
cuts kernel time per request from 11.4 to 3.0 microseconds. Received stream
data is handed over with a memcpy rather than a byte at a time, worth 24 % of
HTTP/3's userspace instructions.

Against nginx: 988k requests/sec on HTTP/1.1, 1.50M on HTTP/2, 2.31M on
HTTP/3 - 12 %, 22 % and 82 % ahead of the nearest of wrk and h2load.

The container suite grew from 73 checks to 205: every endpoint is now also
sent a request body and a large header block, two of them answer with a
quarter of a megabyte, and one demands a Retry. Four of the six bugs above
were found by those.