Interop parity with libnghttp2

This page records every interop cross-test PureHTTP2.jl runs against libnghttp2 via Nghttp2Wrapper.jl. The cross-test group lives in test/interop/testitems_interop.jl and is verified in CI on every push to main by a dedicated interop job.

From Milestone 4 onward, these cross-tests are the regression contract for PureHTTP2.jl's wire behaviour. Any change to PureHTTP2.jl that breaks one of them fails CI at PR time. This is how PureHTTP2.jl fulfills constitution Principle III (Specification Conformance & Reference Parity).

Known-green versions

Validated against:

  • Nghttp2Wrapper.jl commit a3dbdfb548c3d4bfbf4ddfce2a835a990f19dcc2
  • nghttp2_jll v1.64.0+1 (bundles libnghttp2 1.64.0)
  • Julia 1.12.6

Interop parity under a different nghttp2_jll version or a different Nghttp2Wrapper.jl revision is not guaranteed until re-validated.

Cross-test matrix

TestElementRFCDirectionVerdictNotes
Interop: preface bytesConnection prefaceRFC 9113 §3.4nghttp2 emits → PureHTTP2.jl comparesbyte-identical24-byte client magic, byte-for-byte equality
Interop: frame type constantsFrame type enumRFC 9113 §6cross-check (numeric equality)byte-identical10 frame types: DATA, HEADERS, PRIORITY, RSTSTREAM, SETTINGS, PUSHPROMISE, PING, GOAWAY, WINDOW_UPDATE, CONTINUATION
Interop: flag constantsFrame flag bitsRFC 9113 §6cross-check (numeric equality)byte-identicalNONE, ENDSTREAM, ENDHEADERS, ACK. PADDED (0x08) and PRIORITY (0x20) exist in PureHTTP2.jl but are not exported as constants by Nghttp2Wrapper.jl at the pinned commit — they are covered by integration tests instead
Interop: settings parameter constantsSETTINGS identifiersRFC 9113 §6.5.2cross-check (numeric equality)byte-identicalAll 6 identifiers
Interop: HPACK encode nghttp2 → decode PureHTTP2.jlHPACK header compressionRFC 7541nghttp2 encodes → PureHTTP2.jl decodessemantic-equivalentHPACK is not byte-unique; comparison is on decoded header lists
Interop: HPACK encode PureHTTP2.jl → decode nghttp2HPACK header compressionRFC 7541PureHTTP2.jl encodes → nghttp2 decodessemantic-equivalentReverse direction
Interop: SETTINGS round-tripSETTINGS frameRFC 9113 §6.5nghttp2 emits → PureHTTP2.jl parsesbyte-identicalExercises MAXCONCURRENTSTREAMS and INITIALWINDOWSIZE
Interop: PING round-tripPING frameRFC 9113 §6.7nghttp2 emits → PureHTTP2.jl parsesbyte-identical8-byte opaque payload round-trips byte-for-byte
Interop: GOAWAY last-stream-id and error codesGOAWAY frameRFC 9113 §6.8nghttp2 emits → PureHTTP2.jl parsesbyte-identicalExercises NOERROR, PROTOCOLERROR, CANCEL across 3 server-initiated last-stream-ids
Interop: DATA frame END_STREAMDATA frameRFC 9113 §6.1PureHTTP2.jl self-round-trip + PADDED layout checkbyte-identicalEncoder/decoder round-trip plus validation of PAD_LENGTH layout per RFC 9113 §6.1
Interop: WINDOW_UPDATE handshakeWINDOW_UPDATE frameRFC 9113 §6.9both directionsbyte-identicalnghttp2 emits + PureHTTP2.jl parses; PureHTTP2.jl emits + self-parses
Interop: RST_STREAM error code propagationRST_STREAM frameRFC 9113 §6.4PureHTTP2.jl → bit-level wire formatbyte-identical4-byte big-endian error code for CANCEL, INTERNALERROR, PROTOCOLERROR, STREAM_CLOSED

Total: 12 cross-test items, 105 individual assertions, all passing at Milestone 4. The minimum cross-test set from the roadmap (8 entries: preface, SETTINGS, HEADERS+HPACK, DATA, WINDOWUPDATE, RSTSTREAM, GOAWAY, PING) is fully covered.

Deliberate divergences

(none at this milestone)

All 12 cross-tests produce either byte-identical or semantic-equivalent results. No cases were identified where PureHTTP2.jl and libnghttp2 emit legitimately different RFC-compliant bytes.

Upstream bugs discovered

(none at this milestone)

No libnghttp2, nghttp2_jll, or Nghttp2Wrapper.jl bugs surfaced during the Milestone 4 interop migration. See upstream-bugs.md for any bugs discovered in later milestones.

Notes on the cross-test methodology

  • In-memory IO, not TCP sockets. The cross-tests exchange bytes via Nghttp2Wrapper._session_send_all and direct parsing with PureHTTP2.jl's decode_frame, not over a network socket. This is sufficient for frame-level parity validation; real network end-to-end testing is a later milestone's concern.
  • HPACK comparison is on decoded header lists. HPACK encoding is not byte-unique per RFC 7541, so the HPACK cross-tests compare the logical header list after decode, not the encoded byte sequence.
  • Client-role only. All 12 cross-tests use an nghttp2 client session. PureHTTP2.jl's connection layer is currently server-role only (see Connection role signalling), so the cross-tests exchange bytes in the direction client-emits → server-like-parser (PureHTTP2.jl). Client-role symmetry in PureHTTP2.jl is scheduled for Milestone 6, and client-role cross-tests land with it.
  • DATA frame cross-test is a self-round-trip + wire-format check. Submitting raw DATA frames directly to nghttp2 requires a full handshake (preface + SETTINGS exchange + stream opening via HEADERS). Exercising the full handshake in a single @testitem is out of scope at M4. The DATA cross-test therefore validates the wire layout (frame header bytes, PAD_LENGTH layout for padded frames) via PureHTTP2.jl's encoder + decoder, which is RFC-level parity in effect.

How to re-run the interop group locally

Requires Julia ≥ 1.12 (Nghttp2Wrapper.jl's declared minimum). The main PureHTTP2.jl test suite still runs on Julia 1.10+ without this interop env.

julia --project=test/interop -e '
    using Pkg
    Pkg.develop(PackageSpec(path=pwd()))
    Pkg.add(PackageSpec(
        url="https://github.com/s-celles/Nghttp2Wrapper.jl",
        rev="a3dbdfb548c3d4bfbf4ddfce2a835a990f19dcc2"))
    Pkg.instantiate()'
julia --project=test/interop test/interop/runtests.jl

To run a single item by name:

julia --project=test/interop -e '
    using TestItemRunner
    @run_package_tests filter = ti -> ti.name == "Interop: preface bytes"'