v2.1.1
HTTP/1.1 response parsing now comes from
zurl instead of the parser zrk hand-rolled.
What zrk measures does not change. Requests are byte-identical, latency is
recorded the same way, and Transfer/sec counts the same bytes — that last one
verified against the old parser rather than assumed, at 87.00 bytes per request
over ~4000 round trips against a server writing exactly 87.
What could bite you
Framing is stricter now, and three malformed shapes that 2.1.0 accepted are
read errors in 2.1.1. All three are servers doing something wrong, but if your
target is one of them the symptom is a run that suddenly reports read errors
where it used to report results:
- Bare LF line endings in the head. RFC 9112 says CRLF; the old scanner
tolerated LF alone. If a target does this, you will see it immediately and
everywhere, not intermittently. - A head over 16 KiB in total. The old parser bounded the longest single
header line; zurl scans the head as one contiguous slice, so the connection
read buffer bounds the whole head. Many small headers can now add up to
HeaderTooLongwhere each line individually fit. - Conflicting
Content-Length. Two different lengths on one response is
the request-smuggling primitive, and it is refused rather than silently
resolved to the last one. Duplicates that agree still pass.
Nothing else about a well-formed response changed. Chunked bodies, trailers,
bodyless statuses, Transfer-Encoding beating Content-Length, HEAD framing
and keep-alive all behave as they did.
Why change something that worked
This was the third copy of HTTP/1.1 framing in the org, and the least complete
one had the subtlest job. zurl exists because the other two were wrong in ways
a shared parser makes structurally impossible: one read a chunked body as plain
text for months, splicing hex chunk-size lines into whatever metric line
straddled a 2 KiB boundary; the other leaked on timeout because it could not
cancel a thread. zrk's copy was correct, and being correct in a third place is
still three places to be correct in.
zurl's own design docs set the bar for this — zrk adopts nothing from here
until these benchmarks say parity — and it was measured before it was taken.
The parser came in at 0.55–0.85× the hand-rolled one's cost depending on
response shape. You will not see that in a benchmark: a load generator's inner
loop is syscall-bound, and this is tens of nanoseconds inside a round trip
measured in microseconds.
Only the parser moved. Building the request stays in zrk, because zrk builds it
once and replays the bytes, where zurl re-serializes per call — 93–103 ns
against 2.6 ns, on a path that runs millions of times per run. The HTTP/2 path
is untouched; zurl does not speak it.
Also
zig build test covers 233 tests. Three of them are new, and they pin the
stricter cases above so those stay deliberate rather than incidental.