Architecture

June 29, 2026 ยท View on GitHub

The internals of http-parser: how a single streaming state machine turns raw HTTP bytes into a sequence of callbacks without buffering or allocating, and how the pieces fit together. For usage see README.md; for the repo map and invariants see AGENTS.md.

Shape

Two headers and one compiled state machine:

  http_parser.h        declares the API (init, execute, settings, helpers) and the parser/settings structs
  http_parser_enums.h  the method / status / errno / state / header-state enums, built with enum.h's ENUM()
  http_parser.cc       the state machine: http_parser_execute() plus the helpers
  likely.h             self-contained likely()/unlikely() branch hints (vendored, no config.h)

http_parser.h defines the two structs a consumer touches: http_parser (the parser state, mostly private bitfields plus the read-only method, http_major/minor, status_code, http_errno, and a public data pointer) and http_parser_settings (the table of callback function pointers). Everything else is a free function operating on those.

The streaming state machine

The whole parser is one big function, http_parser_execute(), built as a state machine over an enum state (defined in http_parser_enums.h: s_start_req, s_req_method, s_req_path, s_header_field, s_body_identity, s_chunk_size, and dozens more). Each call walks the bytes you hand it, one at a time, advancing the state and firing callbacks at the right boundaries. The current state lives in the http_parser struct between calls, which is what makes the parse independent of how the input was framed: a byte-at-a-time feed and a single full-buffer feed drive the same transitions and produce the same callbacks.

It is zero-copy. The data callbacks (on_url, on_header_field, on_header_value, on_body) are handed a pointer into your buffer and a length; the parser marks the start of a run (e.g. url_mark = p) and emits the slice when the run ends or the buffer does. Because a run can straddle a buffer boundary, a single logical value can be reported in several callbacks, so a consumer appends rather than assigns.

Callbacks and message flow

A request drives the callbacks in this order: on_message_begin, then on_url, then alternating on_header_field / on_header_value for each header, then on_headers_complete, then on_body (zero or more times), then on_message_complete. Chunked bodies add on_chunk_header / on_chunk_complete around each chunk. The notify callbacks (no data) return non-zero to abort; on_headers_complete additionally uses its return value to signal "no body" (1, for HEAD) or "no body and no more messages" (2, for CONNECT).

Bodies come in two forms the machine handles distinctly. A Content-Length body is identity-encoded: content_length is loaded from the header and counted down as body bytes are delivered, reaching zero at on_message_complete (so reading content_length after a complete parse gives 0, not the header value). A Transfer-Encoding: chunked body loops through s_chunk_size (hex length) -> s_chunk_data -> back, reassembling the chunks into a continuous on_body stream and terminating on the zero-length chunk.

The enums

http_parser_enums.h is where this copy diverges most visibly from a stock parser. The status codes, request methods, errno values, and the internal state and header-state enums are all defined through the ENUM() macro from the sibling enum-reflection library, rather than plain C enums. Compiled as C++ (which is how this library builds), ENUM() expands to a real enum plus reflective constexpr accessors, so the dependency is load-bearing: it is what makes "enum.h" a required include, not just a path fixup. The method map also carries Xapiand's extra verbs (CHECK, COMMIT, COPY, SEARCH, UPDATE, UPSERT, and the rest) appended after the standard HTTP set.

http_method_str(), http_status_str(), http_errno_name(), and http_errno_description() are plain table lookups built from the same X-macro maps (HTTP_METHOD_MAP, HTTP_STATUS_MAP, HTTP_ERRNO_MAP) that define the enums, so the names and the values cannot drift apart. http_errno_name() returns the HPE_-prefixed token name (e.g. "HPE_INVALID_METHOD"), since the table is generated as "HPE_" #n.

Errors and pausing

On malformed input the machine sets parser->http_errno to the matching HPE_* value and stops; http_parser_execute() returns the number of bytes consumed before the stop, which is short of the input length. A consumer checks HTTP_PARSER_ERRNO(parser) after every call. The parser can also be paused mid-stream with http_parser_pause(), which sets HPE_PAUSED so the next execute() knows to resume rather than treat the stop as an error.

Why this shape

The design choice that drives everything is "parse off the wire without owning the buffer." A streaming state machine with callbacks means the caller decides what to keep and where to put it, the parser never allocates, and a message can be processed as it arrives instead of after it is fully buffered. That is the right shape for a server reading from a socket in arbitrary-sized reads, which is what Xapiand uses it for. The cost is the callback-and-append contract: values can arrive in pieces, and the consumer has to stitch them. The library keeps that contract identical to upstream Joyent/Node so the parser can be reasoned about against the large body of existing knowledge about it.