Repository navigation
feat: HTTP/2 + HPACK protocol with h2/h2c negotiation and server push - #172
Open
akashshah-dev wants to merge 14 commits into
Open
akashshah-dev wants to merge 14 commits into
akashshah-dev wants to merge 14 commits into
Conversation
Implements header compression for HTTP/2: - static (RFC 7541 Appendix A) and dynamic tables with indexed / literal representations and Huffman-coded strings. - incremental indexing and table-size update (SETTINGS_HEADER_TABLE_SIZE), with an explicit list-size limit for the decoder (anti-DoS). - incremental decoding across fragmented blocks (HEADERS + CONTINUATION) so header-block contexts stay in sync across interleaved streams.
A callback based parser for the HTTP/2 framing layer: - 9 octet frame header parsing with 24-bit payload length and 31-bit stream id. - zero-copy payload reporting when the whole frame is available in the input buffer, with internal accumulation for fragmented frames. - immediate frame reporting for zero-length payloads and an error callback when a frame exceeds the configured maximum frame size.
The connection state machine built on the framing parser: - client preface validation and server / client SETTINGS exchange (RFC 9113 §3.5). - stream state tracking (idle, reserved, open, half-closed, closed) with RFC-compliant frame validation per state (§5.1, §5.2). - flow control: connection and stream windows, WINDOW_UPDATE, and SETTINGS_INITIAL_WINDOW_SIZE recalculation (§5.2, §6.9). - PING (§6.7), GOAWAY (§6.8), RST_STREAM (§6.4) with outstanding-PING and concurrent-stream anti-DoS caps. - PUSH_PROMISE (§6.6): send-side emission and receive-side decoding through the shared HPACK context, keeping the decoder in sync with subsequent header blocks (HEADERS + CONTINUATION).
Integrates the connection core with the http_s request/response API: - client and server roles: decodes request / response header blocks into http_s objects dispatched through the standard handlers (RFC 9113 §8). - streaming responses (memory and file backed) buffered per stream and flushed as the peer's WINDOW_UPDATE replenishes the send windows. - request lifecycle with deferred cleanup (zombie free after the current parse returns) so handlers may free or complete the request safely. - server push (RFC 9113 §8.4): http_push_data / http_push_file now return 0 and push a resource via PUSH_PROMISE with a promised-stream response (content-type and content-length headers, END_STREAM). - client role records and accepts pushed responses, routing their header blocks and DATA without touching the request template.
The server now accepts HTTP/2 over all three RFC 9113 §3.1-3.3 paths: - ALPN: http_listen registers the "h2" protocol after "http/1.1", which stays the default fallback for clients that do not offer HTTP/2. - h2c prior knowledge (§3.3): http1_on_data_first_time detects the PRI preface in the first read and hands the buffered bytes to the HTTP/2 protocol via fio_attach. - Upgrade: h2c (§3.2): http1_on_request answers valid upgrade requests with 101 Switching Protocols, applies the HTTP2-Settings payload as a SETTINGS frame and swaps protocols with any leftover frames. Malformed upgrade requests (invalid HTTP2-Settings, a request body, or a disabled push) are rejected with 400 before the protocol swap.
- tests/http2.c: end-to-end protocol test over http_listen covering the direct server, h2c prior knowledge and Upgrade: h2c negotiation paths, streaming responses (echo and large bodies), and server push verification on all three connections. - tests/http2-conn.c: connection layer unit tests - stream state machine, flow control, settings, PING / GOAWAY / RST_STREAM and PUSH_PROMISE. - tests/http2-frames.c: framing parser unit tests. - tests/http2-hpack.c: HPACK unit tests including the RFC 7541 Appendix C decode / encode vectors. - CMakeLists.txt: add lib/facil/http/http2.c to the source list.
Reserve the top of the per-block scratch budget for the accumulated fragment block, so decoded output can't overwrite it mid-parse and the overflow fails cleanly with a COMPRESSION error instead of corrupting the block. Make hpack_context_encode transactional: the dynamic table is only mutated once the block encodes successfully, while the index references still mirror the decoder's table state by simulating the block's own additions and their evictions (RFC 7541 4.1).
A frame payload larger than the accumulation buffer could only be delivered via the zero-copy path (the whole frame in a single parse call). Rejecting such frames in the partial-accumulation stage prevents an overflow of the internal buffer, while the connection layer clamps the advertised SETTINGS_MAX_FRAME_SIZE to the buffer capacity.
…mits Separate the local and peer stream-id namespaces so PUSH_PROMISE reservations can't block or alias client streams, and track the header block stream id in a full 32-bit field. Validate incoming header blocks (RFC 9113 8.1.2, 8.2, 8.3): pseudo header ordering, duplicates and unknowns, lowercase names, forbidden connection-specific fields, :status digits, mandatory pseudo headers, and Host vs :authority agreement. Enforce the header-list size (32 + name + value per field) and reject a server ENABLE_PUSH on a client connection. Malformed blocks are a stream error; connection-specific fields are a connection error. Charge the full padded DATA payload to the flow-control windows and replenish the connection and stream windows with the padded size, and clamp SETTINGS_MAX_FRAME_SIZE / HEADER_TABLE_SIZE to the supported limits, propagating a header-table failure as a protocol error.
Use fully valid request/response/push header blocks in the connection tests, adapt the data callback to the padded payload size, and add regressions for: stream-id namespace separation, header-block state on streams over 255, padded DATA flow-control accounting, header validation (uppercase, pseudo ordering, unknown pseudo, missing mandatory, connection-specific), header-list limits, fragmented-block scratch overflow, the max-frame clamp, and the GOAWAY last-stream-id reflecting the peer namespace.
The h2c upgrade (RFC 9113 §3.2) previously relied on fio_base64_decode, which is a standard Base64 decoder: it accepts '+' and '/' and treats the whole alphabet as off-by-one, so every HTTP2-Settings payload was silently dropped and malformed payloads were accepted. Replace it with a strict RFC 4648 §5 Base64URL decoder and require the decoded settings to be a multiple of 6 bytes (rejecting a leftover, non-SETTINGS payload). Additionally: - reject duplicate HTTP2-Settings headers (collected into an array), which previously bypassed the upgrade gate entirely - respond with a 400 for declined upgrades (a request with a body or a transfer-encoding must not be upgraded, and the Upgrade token must be exactly "h2c"), instead of falling through to the generic upgrade machinery - mark the rejection response as keep-alive: the request's "Connection: Upgrade, HTTP2-Settings" header would otherwise mark the response as connection:close, dropping the queued response bytes (legacy HTTP/1.1 behavior)
Cover the RFC 9113 §3.2 h2c upgrade with seven adversarial cases: a
fragmented request with a preface and leftover HEADERS frame (101), an
upgrade request with a body (400), a multi-token Upgrade header (400),
malformed Base64URL (400), invalid ENABLE_PUSH and INITIAL_WINDOW_SIZE
settings values (400), and duplicate HTTP2-Settings headers (400).
Also fixes the negotiation test payload ("AAMAAABkAAR____" is not valid
Base64 and its 15 bytes are not a multiple of 6) and re-runs the
completion check from every event so the test cannot miss the final
state transition.
akashshah-dev
marked this pull request as ready for review
August 20, 2026 16:33
The report string had ten format specifiers for nine arguments, which -Werror=format rejects. Drop the redundant counter.
Add a CI workflow with three jobs: - build: gcc and clang on Ubuntu, clang on macOS - environment sanity (make vars, DEBUG=1 make vars), library build (make lib), and the HTTP/2 test suites when present on the branch. - sanitizers: ASan + UBSan build and test run on Ubuntu. - werror: -Werror compile gate for the HTTP/2 test files. The HTTP/2 suites are only executed when the branch provides them, so the workflow validates both the fork's master and feature branches.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds complete HTTP/2 (RFC 9113) and HPACK (RFC 7541) support to facil.io's HTTP server, with a single protocol object serving both client and server roles.
What's included
HPACK header compression (RFC 7541)
HTTP/2 framing parser (RFC 9113 §4–6)
HTTP/2 connection layer (RFC 9113)
HTTP/2 protocol object
http_sobjects dispatched through the standard request/response handlers (RFC 9113 §8).http_push_data/http_push_filenow return 0 and deliver a pushed resource (PUSH_PROMISE + promised-stream response); client role records and accepts pushes.Server negotiation (RFC 9113 §3.1–3.3)
h2registered afterhttp/1.1, which remains the fallback.fio_attach.Upgrade: h2c: 101 + protocol swap. TheHTTP2-Settingspayload is strictly decoded (RFC 4648 §5 Base64URL, multiple-of-6 length) and applied as a SETTINGS frame; duplicateHTTP2-Settingsheaders, a body or transfer-encoding, a multi-tokenUpgrade, malformed Base64URL and invalid settings values are all rejected with a 400.Testing
Four new suites; all pass in normal, debug (
-Werror) and ASan+UBSan (zero runtime errors) builds:tests/http2.c— end-to-end overhttp_listen: direct, prior-knowledge and upgrade paths; echo/large streaming bodies; server push verified on all three connections; seven adversarial h2c upgrade cases (fragmented request with leftover frames, body, multi-token Upgrade, malformed Base64URL, invalid ENABLE_PUSH and INITIAL_WINDOW_SIZE, duplicate HTTP2-Settings).tests/http2-conn.c— stream states, flow control, settings, PING/GOAWAY/RST_STREAM, PUSH_PROMISE.tests/http2-frames.c— framing parser unit tests.tests/http2-hpack.c— HPACK unit tests incl. RFC 7541 Appendix C vectors.Commit history
5e790085feat(http2): add HPACK (RFC 7541) encoder and decoder1ff81566feat(http2): add HTTP/2 framing parser (RFC 9113 §4-6)cc8029d4feat(http2): add HTTP/2 connection layer (RFC 9113)2ab4ab3afeat(http2): add HTTP/2 protocol object with server push15f94aebfeat(http2): negotiate HTTP/2 in the server accept pathb6a8fac0test(http2): add integration and unit test suites2ebace54fix(hpack): reserve scratch space and make encoding transactional69e2936cfix(http2): reject partial frames larger than the parser buffer47cb9458fix(http2): validate headers, account the header list, and enforce limits8fb0c1f8test(http2): cover the PR review regressions5240dbe1fix(http1): strictly decode and validate the h2c upgrade requeste2f3477ctest(http2): exercise h2c upgrade adversarial casesCloses #2, #3