Skip to content

feat: HTTP/2 + HPACK protocol with h2/h2c negotiation and server push - #172

Open
akashshah-dev wants to merge 14 commits into
boazsegev:masterfrom
devstroop:feature/http2-hpack
Open

akashshah-dev wants to merge 14 commits into
boazsegev:masterfrom
devstroop:feature/http2-hpack

Conversation

@akashshah-dev

@akashshah-dev akashshah-dev commented Aug 20, 2026 •

Copy link
Copy Markdown

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)

  • Static/dynamic tables, indexed and literal representations, Huffman-coded strings, table-size updates, and incremental decoding across fragmented blocks so encoder/decoder contexts stay in sync across interleaved streams.
  • Decoder side enforces an explicit dynamic-table size limit (anti-DoS).
  • Encoding is transactional and reserves scratch space up front, so an encode that would exceed the output budget leaves the encoder context unchanged.

HTTP/2 framing parser (RFC 9113 §4–6)

  • 9-octet frame headers, 24-bit lengths, 31-bit stream IDs, zero-copy payload reporting, internal accumulation for fragmented frames, and max-frame-size enforcement.
  • Partial frames larger than the parser buffer are rejected instead of being lost or truncated.

HTTP/2 connection layer (RFC 9113)

  • Client preface validation, SETTINGS exchange, stream state machine with per-state frame validation (§5.1–5.2).
  • Flow control: connection/stream windows, WINDOW_UPDATE, SETTINGS_INITIAL_WINDOW_SIZE recalculation (§5.2, §6.9).
  • PING, GOAWAY, RST_STREAM with outstanding-PING and concurrent-stream caps.
  • PUSH_PROMISE (§6.6) emission and receive-side decoding through the shared HPACK context.
  • Incoming header blocks are validated (pseudo-header ordering, forbidden fields, per-frame size limits) and counted against the header-list-size limit before the request is dispatched.

HTTP/2 protocol object

  • Client and server roles: header blocks decoded into http_s objects dispatched through the standard request/response handlers (RFC 9113 §8).
  • Streaming responses (memory- and file-backed) buffered per stream and flushed on peer WINDOW_UPDATE.
  • Deferred (zombie) request cleanup so handlers can safely free or complete requests mid-parse.
  • Server push (§8.4): http_push_data / http_push_file now 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)

  • ALPN: h2 registered after http/1.1, which remains the fallback.
  • h2c prior knowledge: PRI preface detected in the first read, buffered bytes handed to the HTTP/2 protocol via fio_attach.
  • Upgrade: h2c: 101 + protocol swap. The HTTP2-Settings payload is strictly decoded (RFC 4648 §5 Base64URL, multiple-of-6 length) and applied as a SETTINGS frame; duplicate HTTP2-Settings headers, a body or transfer-encoding, a multi-token Upgrade, 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 over http_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

  1. 5e790085 feat(http2): add HPACK (RFC 7541) encoder and decoder
  2. 1ff81566 feat(http2): add HTTP/2 framing parser (RFC 9113 §4-6)
  3. cc8029d4 feat(http2): add HTTP/2 connection layer (RFC 9113)
  4. 2ab4ab3a feat(http2): add HTTP/2 protocol object with server push
  5. 15f94aeb feat(http2): negotiate HTTP/2 in the server accept path
  6. b6a8fac0 test(http2): add integration and unit test suites
  7. 2ebace54 fix(hpack): reserve scratch space and make encoding transactional
  8. 69e2936c fix(http2): reject partial frames larger than the parser buffer
  9. 47cb9458 fix(http2): validate headers, account the header list, and enforce limits
  10. 8fb0c1f8 test(http2): cover the PR review regressions
  11. 5240dbe1 fix(http1): strictly decode and validate the h2c upgrade request
  12. e2f3477c test(http2): exercise h2c upgrade adversarial cases

Closes #2, #3

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 akashshah-dev changed the title feature: http2/hpack feat: HTTP/2 + HPACK protocol with h2/h2c negotiation and server push Aug 20, 2026
@akashshah-dev
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

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Recommendations for path/param matching

1 participant