zttp¶
A sans-IO HTTP parser for Python with a Zig core! ⚡
Source Code: https://github.com/Kludex/zttp
zttp is a sans-IO HTTP parser whose engine is written in Zig. It speaks HTTP/1.1, HTTP/2, and HTTP/3, and it does no I/O of its own: you feed it bytes and pull out events, and you ask it for bytes to send. It never touches a socket, so it works with any I/O you like (asyncio, threads, a green-thread library, a test harness), and it's a joy to test.
It's the same idea as h11, with a hand-written Zig engine underneath instead of pure Python. It has no dependencies.
The key features are:
- Sans-IO: a clean, event-based API. Feed bytes with
receive_data, pullRequest/Data/EndOfMessageevents withnext_event. No callbacks, no sockets, no surprises. - HTTP/1.1, HTTP/2, and HTTP/3: the same event API for all three. Pass
protocol=zttp.HTTP2and you get multiplexed streams, HPACK, and flow control (see HTTP/2); passprotocol=zttp.HTTP3and a from-scratch QUIC transport feeds the same events from UDP datagrams (see HTTP/3). - Fast: a hand-written Zig engine with branch-light scanning and minimal allocation (see Performance for the numbers).
- Safe: strict by default. It defends against request smuggling, rejects bare
LFline endings, bounds every buffer, and ships in Zig's safety-checked build. - Typed: a
py.typedpackage with full type hints. Your editor knows every event field. - Tested: a high-level test suite at 100% coverage, plus the Zig core's own tests and an adversarial-input fuzz harness.
Experimental
zttp is experimental. The API and behaviour may change at any time, and it is not yet ready for production use.
Installation¶
Requirements
zttp needs CPython 3.10+ and runs on Linux, macOS, and Windows.
Example¶
Let's parse an HTTP request. You play the server: bytes come in, events come
out. A Connection takes your role - SERVER to receive requests or CLIENT
to receive responses.
import zttp
conn = zttp.Connection(zttp.SERVER)
conn.receive_data(
b"GET /hello?name=you HTTP/1.1\r\n"
b"Host: example.com\r\n"
b"\r\n"
)
event = conn.next_event()
print(event.method, event.target)
#> b'GET' b'/hello?name=you'
print(conn.next_event())
#> EndOfMessage(trailers=[])
Feed receive_data whatever bytes you have - a whole request, half a request, or
a single byte - and zttp buffers and resumes. Each next_event call returns the
next complete event, or the NEED_DATA sentinel when it needs more bytes.
Run it:
That's it. The buffering, the header parsing, the body framing: all Zig. 🎉
Tip
Notice there were no callbacks. You don't register on_header /
on_body functions and lose control of the flow. You pull events when
you are ready. That's what sans-IO means. Read
Architecture for the why.
Where to go next¶
-
The 30-second tour of the read side: feed bytes, pull events.
-
The write side: build requests and responses, get bytes to send.
-
The same API, multiplexed: streams, flow control, and the control events.
-
The same events again, fed by UDP datagrams through a QUIC transport.
-
What sans-IO is, and why a parser should do no I/O.
-
The benchmarks, the methodology, and the honest caveats.