We do not ask your client whether it handled the protocol correctly. We watch what it actually did on the wire.
Point a client library at it and the server misbehaves on purpose — a reserved frame type, a duplicated setting, a control stream opening with the wrong frame — then records how your client answered. Every verdict comes from what the server observed, because a library that crashes on an unknown frame is in no position to report on itself. Free, no account, open to any client.
The QUIC Interop Runner tests implementations against each other in a lab matrix. This is the other thing: the awkward corners, on demand, against your client.
Results for several well-known implementations are published in the client matrix — what curl, Chromium, quic-go, aioquic and quinn each do when the server misbehaves.
Unlike an interoperability test, this one deliberately sends your client protocol edge cases, illegal messages, GREASE, awkward state transitions and hostile network conditions — and judges the result from the wire, not from what the client reports about itself.
Offering one requires a server that will misbehave on demand â emit a reserved frame type, duplicate a SETTINGS identifier, open a control stream with the wrong frame. That is not something a CDN customer can arrange, because they do not own the implementation. We fork the QUIC stack, so we can.
What the catalogue covers
QUIC
23 tests
- correctness
- 8
- discretionary
- 7
- resilience
- 5
- interoperability
- 2
- extensibility
- 1
HTTP/3
28 tests
- correctness
- 14
- interoperability
- 5
- discretionary
- 4
- extensibility
- 3
- resilience
- 2
What a result means
Running it
Each test has its own UDP port. Connect to it, and the server records what your client did. Results are filed under a session started from your address, so the test connections need nothing special.
A port per test rather than a path per test, because several anomalies are properties of the endpoint rather than of a connection — the set of QUIC versions offered is decided before any connection exists, so a test of it has to own the whole port.
# start a session
SESSION=$(curl -sX POST https://conformance.pqcrypta.com/session | jq -r .id)
# walk the catalogue
curl -s https://conformance.pqcrypta.com/catalog.json \
| jq -r '.tests[].port' | while read PORT; do
curl -s --http3-only --max-time 15 \
"https://conformance.pqcrypta.com:$PORT/" -o /dev/null
done
# read the result
curl -s https://conformance.pqcrypta.com/report/$SESSION.json | jq .totals
Without jq:
grep -o '"port":[0-9]*' | cut -d: -f2 pulls the ports out
of the catalogue, and the session id is the first
"id" in the /session response.
The report is also available as a page at
/report/<session> and as an SVG badge at
/badge/<session>.svg.
In CI
h3-conformance does the above and exits non-zero if
anything failed. It is a harness rather than a client: it runs a
command you supply, once per test, so whatever your
library connects with is what gets measured.
# drive curl
h3-conformance
# drive your own client
h3-conformance --client 'my-client --url {url}'
# one area, machine-readable
h3-conformance --filter h-qpack --json
Exit 0 clean, 1 a test failed,
2 the suite was unreachable — distinct so an
outage never reads as a regression in your client. Inconclusive
results never fail the run.
| Option | Default | What it does |
|---|---|---|
--host | conformance.pqcrypta.com | Which suite to run against. |
--client | curl | Command run once per test. {url} and {port} are substituted; its exit status is ignored, because a client failing a test frequently should exit non-zero and that is a result, not an error. |
--filter | — | Run only tests whose id contains this, e.g. h-qpack. |
--timeout | 30 | Seconds to allow each client invocation before giving up on it. |
--json | off | Print the report as JSON instead of text. |
--quiet | off | Only print failures and the summary. |
The report
A session collects results as you walk the ports, and is readable
three ways: as a page at /report/<session>, as JSON
at /report/<session>.json, and as an SVG badge at
/badge/<session>.svg. All three are public and
CORS-open, so CI can fetch them without a key.
{
"session": "8fbecf53c82f7c6481ecca51727c6742",
"catalog_size": 51,
"totals": { "pass": 22, "fail": 0, "inconclusive": 5, "not_run": 0 },
"by_class": { "correctness": { "pass": 6, "fail": 0, "inconclusive": 3 }, ... },
"results": [
{ "id": "h-grease-frame", "class": "extensibility", "tier": "http3",
"spec": "RFC 9114 §7.2.8", "verdict": "pass",
"detail": "Ignored the unrecognised element and completed the follow-up request." }
]
}
Test ids are stable and appear in the URL, the JSON and
the badge, so they are safe to pin a CI assertion to.
Embedding the badge

A badge is a picture of one run, not a live status: re-run the suite and use the new session to refresh it.
How results find their session
By default results are filed under the session started from your
address, which is why the walk above needs nothing special. If your
tests run from several machines, or from behind a shared address,
connect with the server name
<session>.conformance.pqcrypta.com instead
— the session travels in the SNI, and results follow it rather
than the address.
A session expires an hour after it was created, not an hour after its last result — activity does not extend it. Start the session at the beginning of the walk rather than long before, and fetch the report while the run is fresh; nothing is archived.
How a verdict is reached
The server is the judge. A library that crashes on an unknown frame is in no position to report on itself, so every verdict comes from what the server observed.
Every test ends with a liveness probe: after the anomaly, the server expects one ordinary request on the same connection. For the extensibility tests this is the whole game â "ignored it" and "died on it" look identical on the wire up to that point, and without the probe a crashed client would read as compliant.
A failure needs positive evidence. It is tempting to read
“no rejection arrived” as “the client accepted it”,
and that inference is unsound. Where the anomaly rides in the response your
client had to read to be served, a completed exchange does show it was seen.
Where it went to the control stream — unidirectional, and read on no
particular schedule — a one-shot request can finish before the stream
is picked up at all, which looks identical to accepting the violation. The
same is true of a rejection whose CONNECTION_CLOSE is lost in
transit, since QUIC only repeats that frame in answer to an incoming packet.
Neither case is reported as a failure.
The classes exist because the required answer is not the same everywhere, and sometimes it is the opposite. Ignoring a reserved HTTP/3 frame is a pass — reserved frame types exist precisely to be ignored. Ignoring a reserved QUIC frame is a failure, because QUIC reserves no ignorable frame types and an unknown frame carries no length to skip it by. Two tests that look alike on the wire require opposite behaviour, so a single pass/fail rule would score one of them backwards.
Extensibility
Ignore something unrecognised and carry on. Rejecting it is the failure â that is how protocols ossify.
Correctness
Reject something invalid, with the specific error code the RFC names. Accepting it is the bug — but a failure here is only recorded when your client demonstrably read the anomaly, never merely because no rejection arrived.
Interoperability
Correctly decode something valid but demanding â Huffman-coded fields, a dynamic-table reference, trailers.
Resilience
Recover â retry, re-probe, migrate â rather than give up.
Discretionary
The RFC permits more than one answer. Both pass; the report says which yours chose.
Inconclusive
The run never put your client in the situation the test is about. It proves nothing either way, and says so.
The catalogue
Read live from the suite, so this table cannot drift from what is actually served. Every entry cites the clause it exercises.
HTTP/3 layer
| Test | Port | Class | Specification |
|---|---|---|---|
h-grease-settings
Reserved SETTINGS identifier (0x1f¡N+0x21)
Ignore the unknown setting and complete the request.
|
4473 | Extensibility | RFC 9114 §7.2.4.1 |
h-grease-frame
Reserved frame type on the response stream
Skip the frame using its length and read the response that follows.
|
4474 | Extensibility | RFC 9114 §7.2.8 |
h-reserved-uni-stream
Unidirectional stream with a reserved stream type
Abort reading the stream or discard it â §6.2.3 permits either. What it forbids is treating it as meaningful, or as fatal to the connection.
|
4475 | Extensibility | RFC 9114 §6.2.3 |
h-duplicate-setting
SETTINGS containing the same identifier twice
Either reject with H3_SETTINGS_ERROR or ignore the repeat â the specification says a receiver MAY treat this as an error, so both are conformant.
|
4476 | Discretionary | RFC 9114 §7.2.4 |
h-control-frame-unexpected
A DATA frame on the control stream
Close the connection with H3_FRAME_UNEXPECTED.
|
4477 | Correctness | RFC 9114 §7.2.1 |
h-missing-settings
Control stream whose first frame is not SETTINGS
Close the connection with H3_MISSING_SETTINGS.
|
4478 | Correctness | RFC 9114 §6.2.1 |
h-second-control-stream
A second control stream opened by the server
Close the connection with H3_STREAM_CREATION_ERROR.
|
4479 | Correctness | RFC 9114 §6.2.1 |
h-qpack-dynamic-table
Field lines referencing dynamic table insertions
Apply the encoder-stream insertions and decode the headers correctly.
|
4480 | Interoperability | RFC 9204 §4.3.3, §4.5.2 |
h-qpack-huffman
Huffman-coded field lines with maximal padding
Decode without error. Padding of up to 7 bits is legal, not corruption.
|
4481 | Interoperability | RFC 9204 §4.1.2, RFC 7541 §5.2 |
h-oversized-field-section
Field section larger than the client's advertised maximum
Handle it as an error against that one request, not the whole connection.
|
4482 | Interoperability | RFC 9114 §4.2.2 |
h-trailers
Trailing field section after the body
Deliver the trailers to the application after the body completes.
|
4483 | Interoperability | RFC 9114 §4.1 |
h-early-hints
103 Early Hints before the final response
Treat 103 as informational and keep reading for the final response. RFC 9110 §15.2 makes this a MUST: a client "MUST be able to parse one or more 1xx responses received prior to a final response, even if the client does not expect one". Ignoring the hints is fine â a user agent MAY do that â but closing the connection is not.
|
4484 | Resilience | RFC 9110 §15.2, RFC 8297 |
h-goaway
GOAWAY sent mid-connection
Stop opening requests, finish those in flight, and retry idempotent ones elsewhere.
|
4485 | Resilience | RFC 9114 §5.2 |
h-max-push-id
MAX_PUSH_ID sent by the server
Reject with H3_FRAME_UNEXPECTED. MAX_PUSH_ID travels client to server only, so a server sending one is using a frame in a direction the specification does not allow.
|
4486 | Correctness | RFC 9114 §7.2.7 |
h-settings-on-request-stream
SETTINGS frame on a request stream
Close the connection with H3_FRAME_UNEXPECTED. SETTINGS belongs to the control stream alone: §7.2.4 says that if an endpoint receives one on a different stream it "MUST respond with a connection error of type H3_FRAME_UNEXPECTED".
|
4490 | Correctness | RFC 9114 §7.2.4 |
h-data-before-headers
DATA frame before any HEADERS on the response stream
Close the connection with H3_FRAME_UNEXPECTED. A response begins with a field section, and §4.1 makes "receipt of an invalid sequence of frames" a connection error of that type â a body arriving before the headers that describe it is exactly that.
|
4491 | Correctness | RFC 9114 §4.1 |
h-cancel-push-unsolicited
CANCEL_PUSH for a push ID that was never promised
Close the connection with H3_ID_ERROR. No MAX_PUSH_ID was granted, so every push ID is greater than currently allowed, and §7.2.3 requires a CANCEL_PUSH referencing one to be treated as a connection error of that type.
|
4492 | Correctness | RFC 9114 §7.2.3 |
h-qpack-blocked-stream
Field section that blocks until the encoder stream catches up
Hold the field section, apply the insertions when they arrive on the encoder stream, and complete the request. §2.2.1 makes a section whose Required Insert Count exceeds the decoder's Insert Count a blocked stream â something to be waited on, not an error.
Only run when the client advertised both a table capacity and at least one blocked stream; §2.1.2 forbids the encoder from blocking more streams than the decoder promised to support, so a client that permits none cannot be tested on this.
|
4493 | Interoperability | RFC 9204 §2.1.2, §2.2.1 |
h-response-stream-reset
Response stream reset mid-body with H3_REQUEST_CANCELLED
Abandon the partial response â §4.1 says a response cancelled after a partial delivery "SHOULD NOT be used". Whether the connection survives is the client's to choose: §8 lets an endpoint "treat a stream error as a connection error under certain circumstances", so keeping the connection and closing it are both conformant. Stalling is the one wrong answer.
|
4494 | Discretionary | RFC 9114 §8, §4.1 |
h-priority-update
PRIORITY_UPDATE sent by the server
Reject it with H3_FRAME_UNEXPECTED, or ignore it â which is conformant depends on whether you implement extensible priorities at all, and that is not observable from here.
A client that does implement RFC 9218 is bound by §7.2: servers "MUST NOT send PRIORITY_UPDATE frames of either type", and a client receiving one MUST treat it as a connection error of that type. A client that does not implement it sees frame type 0xf0700 as simply unknown, and RFC 9114 §9 requires unknown frame types to be ignored â so ignoring it is equally correct, for a different reason.
Scoring this as a failure either way would accuse one of those two clients of a violation it did not commit, so the report says which answer was given rather than grading it.
|
4498 | Discretionary | RFC 9218 §7.2 |
h-extended-connect
SETTINGS_ENABLE_CONNECT_PROTOCOL with a value outside 0 and 1
Reject it or ignore it, but keep working. RFC 8441 §3 says the value "MUST be 0 or 1" and RFC 9220 carries that into HTTP/3 unchanged â yet neither names a behaviour for a receiver that sees anything else, so both answers are conformant and stalling is not. The setting is how a client learns Extended CONNECT is available, so a WebTransport-capable client has real parsing behind it.
|
4499 | Discretionary | RFC 9220 §3, RFC 8441 §3 |
h-push-promise-unsolicited
PUSH_PROMISE for a push the client never allowed
Close the connection with H3_ID_ERROR. §7.2.7 leaves the maximum push ID unset until the client sends MAX_PUSH_ID, so a server "cannot push until it receives a MAX_PUSH_ID frame" and every push ID is larger than the client has advertised. §7.2.5 is explicit about the answer: a client "MUST treat receipt of a PUSH_PROMISE frame that contains a larger push ID than the client has advertised as a connection error of H3_ID_ERROR".
|
4500 | Correctness | RFC 9114 §7.2.5, §4.6 |
h-datagram-setting-invalid
SETTINGS_H3_DATAGRAM with a value that is neither 0 nor 1
Close the connection with H3_SETTINGS_ERROR. Unusually for a setting, RFC 9297 pins down the invalid-value case rather than leaving it open: the value "MUST be either 0 or 1", and if one "is received with a value that is neither 0 nor 1, the receiver MUST terminate the connection with error H3_SETTINGS_ERROR".
This is the setting that gates HTTP Datagrams and so WebTransport, which means a client that implements either has real parsing behind it rather than an ignored identifier.
|
4501 | Correctness | RFC 9297 §2.1.1 |
h-qpack-encoder-overflow
Encoder stream setting a dynamic table capacity above the client's limit
Close the connection with QPACK_ENCODER_STREAM_ERROR (0x201). §4.3.1 says the new capacity "MUST be lower than or equal to the limit" the decoder advertised, and that a decoder "MUST treat a new dynamic table capacity value that exceeds this limit as a connection error of type QPACK_ENCODER_STREAM_ERROR".
Unlike the other two dynamic-table tests, this one runs against every client: they all advertise a capacity of zero, and any capacity at all exceeds a limit of zero.
|
4502 | Correctness | RFC 9204 §4.3.1, §6 |
h-goaway-increasing
A second GOAWAY naming a larger identifier than the first
Close the connection with H3_ID_ERROR. §5.2 permits multiple GOAWAY frames but requires the identifier in each to be no greater than any previously sent, because the identifier is a promise about what will still be processed and raising it takes that promise back. "Receiving a GOAWAY containing a larger identifier than previously received MUST be treated as a connection error of type H3_ID_ERROR."
|
4503 | Correctness | RFC 9114 §5.2 |
h-push-stream-unpromised
Push stream opened for a push nobody allowed
Close the connection with H3_ID_ERROR. §6.2.2 does not require the push to have been promised first â the stream alone is enough: a client "MUST treat receipt of a push stream as a connection error of type H3_ID_ERROR when no MAX_PUSH_ID frame has been sent", and no client under test sends one.
Distinct from the PUSH_PROMISE test, which puts the same violation in a frame on the control stream. This one opens the push stream itself, which a client has to recognise by its stream type before any frame inside it is read.
|
4504 | Correctness | RFC 9114 §6.2.2 |
h-qpack-static-index-invalid
Field line indexing a static table entry that does not exist
Close the connection with QPACK_DECOMPRESSION_FAILED (0x200). The static table has 99 entries, so index 200 refers to nothing, and §3.1 is explicit: "When the decoder encounters an invalid static table index in a field line representation, it MUST treat this as a connection error of type QPACK_DECOMPRESSION_FAILED."
The static table needs no permission and never changes size, so unlike the dynamic-table tests this one applies to every client.
|
4505 | Correctness | RFC 9204 §3.1, §4.5.2 |
h-qpack-encoder-bad-name-index
Encoder instruction naming a static index that does not exist
Close the connection with QPACK_ENCODER_STREAM_ERROR (0x201). The same bad index means different things depending on where it arrives, and §3.1 says both: on a field line it is QPACK_DECOMPRESSION_FAILED, and "if this index is received on the encoder stream, this MUST be treated as a connection error of type QPACK_ENCODER_STREAM_ERROR".
Paired deliberately with the field-line version on the neighbouring port: a client that answers both with the same code has collapsed a distinction the specification draws twice in one paragraph.
|
4507 | Correctness | RFC 9204 §3.1, §4.3.2 |
QUIC layer
| Test | Port | Class | Specification |
|---|---|---|---|
q-version-negotiation
Version Negotiation offering a reserved version alongside v1
Abandon the connection attempt, or retry with a version both ends support. §6.2 requires a client that supports only one version to abandon it rather than persist.
|
4460 | Correctness | RFC 9000 §6 |
q-retry
Retry packet for source-address validation
Echo the Retry token in a second Initial packet and complete the handshake.
|
4461 | Correctness | RFC 9000 §8.1.2 |
q-reserved-transport-param
Reserved transport parameter (31¡N+27)
Ignore the unknown parameter and complete the handshake normally.
|
4462 | Extensibility | RFC 9000 §18.1 |
q-reserved-frame
Unknown frame type in a 1-RTT packet
Close the connection with FRAME_ENCODING_ERROR. Unlike HTTP/3, QUIC reserves no ignorable frame types â §12.4 makes an unknown frame a connection error, so ignoring it is the failure here.
|
4463 | Correctness | RFC 9000 §12.4 |
q-cid-rotation
NEW_CONNECTION_ID followed by RETIRE_CONNECTION_ID
Adopt the new connection ID, retire the old one, and stay connected.
|
4464 | Resilience | RFC 9000 §5.1 |
q-stateless-reset
Stateless reset
Recognise the token in the last 16 bytes of the datagram, enter the draining period, and send no further packets on the connection. The packet cannot be authenticated, so there is nothing to reply with and nothing to report to the peer â continuing to send is the failure §10.3.1 names.
|
4465 | Correctness | RFC 9000 §10.3 |
q-flow-control
Deliberately tight MAX_DATA and MAX_STREAM_DATA
Respect the limit. Announcing the stall with DATA_BLOCKED or STREAM_DATA_BLOCKED is a SHOULD in §4.1, not a MUST, so a client that stays silent is still conformant.
|
4466 | Discretionary | RFC 9000 §4 |
q-ack-frequency
ACK Frequency extension offered
Negotiate the extension, or ignore it. Either is correct; failing is not.
|
4467 | Discretionary | draft-ietf-quic-ack-frequency |
q-ecn
Packets marked ECT(0)
Echo the ECN counts back in ACK frames carrying an ECN section (type 0x03). §13.4.1 makes this a conditional requirement â an endpoint MUST report the markings it receives "if these are accessible", and explicitly permits an endpoint with no access to the ECN field to report nothing. So counts coming back is a pass, and silence is inconclusive rather than a failure: it cannot be told apart from a path that stripped the codepoint in transit.
|
4468 | Correctness | RFC 9000 §13.4 |
q-pmtu-blackhole
Path MTU black hole above a threshold
Detect the black hole, probe down to a working size, and keep the connection. Path-MTU discovery is driven past the limit on this port, so it engages on every connection.
|
4469 | Resilience | RFC 9000 §14, RFC 8899 |
q-path-challenge
Server-initiated PATH_CHALLENGE
Reply with PATH_RESPONSE carrying the identical 8-byte payload.
|
4470 | Correctness | RFC 9000 §8.2 |
q-zero-rtt-reject
0-RTT rejected after the client sends early data
Reset the state of every stream, including application state bound to them. Section 4.6.2 requires the reset because a rejected 0-RTT means every assumed connection characteristic may have been wrong. It does not require retransmission, which is the application concern, not QUIC's.
This port issues tickets that advertise early data and then declines every offer, so a resuming client sends 0-RTT and always has it refused. The handshake itself completes normally.
|
4471 | Resilience | RFC 9001 §4.6.2 |
q-multipath
A second path offered mid-connection
Use the additional path, or decline it cleanly. Do not abort the connection.
|
4472 | Discretionary | draft-ietf-quic-multipath |
q-key-update
Spontaneous 1-RTT key update
Update to the next key phase and carry on. Once a packet protected with the next phase is processed, §6.2 is a MUST â "The endpoint MUST update its send keys to the corresponding key phase in response" â so the response written after the update has to be read with the new keys and the request completed.
|
4487 | Interoperability | RFC 9001 §6.2 |
q-stream-limit
Stream limits set to the minimum a request needs
Stay inside the advertised limits. Respecting them is a MUST â §4.6 says "Endpoints MUST NOT exceed the limit set by their peer" â but announcing the stall with STREAMS_BLOCKED is only a SHOULD, so a client that stays silent is still conformant.
|
4488 | Discretionary | RFC 9000 §4.6 |
q-loss-recovery
One datagram in twelve dropped once the path is established
Reassemble the stream and deliver the whole body. §2.2 requires an endpoint to buffer data received out of order and deliver it as an ordered byte stream, and §13.3 has the lost data sent again in new STREAM frames â so a response full of gaps must still arrive complete and in order.
|
4489 | Resilience | RFC 9000 §2.2, §13.3 |
q-connection-migration
Datagrams arriving from a second server address
Keep using the address you are already talking to. §9.6 says a client "SHOULD ignore packets received from a server address other than the one it is currently using for sending packets" â a SHOULD, so quietly discarding them and objecting are both conformant. Following the new address is not: nothing is listening there, and a server may only move a client to an address it advertised as its preferred one.
|
4495 | Discretionary | RFC 9000 §9.6 |
q-invalid-transport-param
Transport parameter carrying a value the specification forbids
Close the connection with TRANSPORT_PARAMETER_ERROR. §18.2 makes an ack_delay_exponent above 20 invalid, and §7.4 is a MUST: "An endpoint MUST treat receipt of a transport parameter with an invalid value as a connection error of type TRANSPORT_PARAMETER_ERROR."
Distinct from a *duplicate* parameter, which the same clause makes only a SHOULD â this port sends one parameter, once, with a value outside its permitted range.
The parameter travels in the handshake, so every client that connects here reads it. Only a client that answers with a CONNECTION_CLOSE this endpoint can read is scored: one that simply abandons the handshake is recorded as inconclusive, because a close that was sent and lost looks exactly like one that was never sent.
|
4496 | Correctness | RFC 9000 §7.4, §18.2 |
q-zero-rtt-replay
425 (Too Early) in answer to a request sent as early data
Handle being told the request arrived too early. §5.2 says a user agent "SHOULD retry automatically, but any retries MUST NOT be sent in early data" â so retrying on the 1-RTT keys and handing the 425 back to the caller are both conformant, and the report says which happened. Falling over is not one of the options.
This is the only port that *accepts* early data instead of refusing it, which is what makes the 425 exchange possible at all. What it does not check is §4's rule that unsafe methods must never be sent in early data: reading the method would mean QPACK-decoding the request, which this suite deliberately never does.
Reaching it needs a session ticket from an earlier connection to this same port, so a client that connects once has none.
|
4497 | Discretionary | RFC 8470 §5.2 |
q-ecn-congestion
Path marking packets CE, not just ECT(0)
Report the CE count back in the ECN section of an ACK. §13.4.1 makes this the same conditional requirement as reporting ECT(0) â an endpoint MUST provide feedback about the markings it receives "if these are accessible" â and CE is the marking that matters, since it is how a path says it is congested.
Distinct from the ECT(0) test on the neighbouring port: that one asks whether a client reports markings at all, and this asks whether it distinguishes the one that means something. A client that echoes ECT(0) counts faithfully and never reports a CE has a congestion signal it cannot see.
Silence is inconclusive, not a failure, for the same reason: the ECN field may be inaccessible to the peer, and a path that rewrites the codepoint in transit is indistinguishable from here.
|
4508 | Correctness | RFC 9000 §13.4 |
q-key-update-repeated
A second key update after the first is acknowledged
Follow both updates and carry on. §6.1 lets an endpoint update again once the previous phase has been acknowledged, so a long-lived connection changes keys repeatedly and a client has to track the phase rather than assume one change.
Distinct from the single update on the neighbouring port. An implementation that hardcodes the first transition â treating key phase as a one-way flag rather than a bit that alternates â passes that test and fails here, which is precisely the bug worth finding.
|
4509 | Interoperability | RFC 9001 §6.1, §6.5 |
q-max-streams-credit
No stream credit at first, then MAX_STREAMS mid-connection
Wait for the credit, then open the request. This port grants no bidirectional streams in its transport parameters and issues MAX_STREAMS a moment later. §4.6 is unambiguous that a client may not jump the gun â "Endpoints MUST NOT exceed the limit set by their peer" â and announcing the wait with STREAMS_BLOCKED is a SHOULD, so a client that waits quietly is equally conformant.
Giving up rather than waiting is not scored as a failure: nothing obliges a one-shot client to sit on a connection it cannot use yet, and the report says which it did.
|
4510 | Discretionary | RFC 9000 §4.6 |
q-packet-reordering
Datagrams delivered out of order
Put the stream back in order and deliver the whole body. §2.2 requires an endpoint to be "able to deliver stream data to an application as an ordered byte stream", and says plainly that doing so "requires that an endpoint buffer any data that is received out of order".
Distinct from the loss test, which needs retransmission before the gap can be filled. Nothing is lost here: every byte arrives, some of it early, and a client that assumes arrival order is delivery order will produce a corrupt body or stall waiting for data it already has.
|
4506 | Resilience | RFC 9000 §2.2 |
When a run proves nothing
Every port in the catalogue emits its anomaly—51 of 51. Nothing here is a placeholder.
Whether your run reaches a given anomaly is a separate question, and some depend on what your client happens to do. A few examples, all of which report inconclusive rather than guessing:
- 0-RTT rejection needs early data to reject, which needs a session ticket from an earlier connection to the same port. A client that connects once has none.
- ECN reporting is required only where the ECN field is accessible to the peer, and a network can strip the codepoint in transit. Silence there is not attributable to your client.
- QPACK dynamic table references need the client to have granted the encoder some table capacity. Advertise zero, and there is nothing the server is permitted to reference.
- Connection ID rotation and path validation need a connection that lives long enough, or moves. A single short request does neither.
- An anomaly on the control stream that your client closed without objecting to. The control stream is unidirectional and nothing obliges a client to read it on any schedule, so a one-shot request can finish before it is picked up — which looks exactly like accepting the violation. The server holds the response back for a moment first, to give a conformant reader its chance, but where the two cannot be told apart the result is inconclusive rather than a failure. A suite that reports “accepted a protocol violation” on the strength of silence is guessing.
An inconclusive result says which of these applied. It is never counted as a pass and never as a failure, and it does not fail a CI run.
If your client scores a failure you believe is wrong, say so — a suite that accuses the thing it tests is worth fixing quickly.
Scope, and what this is not
Fifty-one tests is a beginning, not coverage. QUIC and HTTP/3 together are enormous, and the catalogue is chosen rather than exhaustive. What is missing divides into two quite different things, and collapsing them into one list of “not done yet” would overstate what this suite can ever become.
Testable, and not yet written
The backlog is shorter than it was: ECN congestion marking, repeated key updates and mid-connection MAX_STREAMS credit have since been built. What is left is a Retry packet a client ought to discard — RFC 9000 §17.2.5.2 has a client ignore any Retry after the first — and a handful of similar single-clause cases.
Compatible version negotiation is a case of its own, and no longer counted
as backlog. RFC 9000 §6.2 does not define what a client does when the
offered version is one it supports; it says only that “how to perform
version negotiation is left as future work”. RFC 9368 supplies that
work, so the test becomes possible once the stack implements
version_information — and not before. Writing it now
would mean measuring behaviour no clause requires, which is how a
conformance suite starts inventing requirements.
Outside what this vantage point can establish
Not a backlog, and listing it as one would be misleading. These are behaviours a server watching a client cannot legitimately provoke or observe, so a test claiming to cover them would be reporting on something other than what it says.
- QPACK decoder-stream errors. That stream is the client's output. The error exists for an encoder objecting to a decoder's instructions, and nothing here can make a client emit a malformed one.
- WebTransport session handling beyond the setting that gates it. Extended CONNECT is initiated by the client; a generic HTTP/3 client never opens a session, so the port would report that nothing was exercised in perpetuity.
- A client's own migration after a NAT rebind. The server can send a PATH_CHALLENGE — and does — but only the client, or its network, can change the address it sends from.
- The amplification limit. A requirement on servers, so there is nothing about it to ask of the thing under test.
- This is not a certification. Passing every test here means a client handled fifty-one specific situations correctly. It does not mean the implementation is conformant, and nothing on this site should be quoted as though it did.
- A failure is a specification violation, not a security vulnerability. Some may have security relevance; most are simply behaviour a specification prohibits. Calling every failure a vulnerability would make the whole dataset less credible, and would be unfair to the implementations measured.
- It tests clients, not servers. For the other direction — what a server advertises and actually does — the HTTP/3 scanner is the tool.
- The suite is itself an implementation, and so is subject to the same doubt as anything it measures. A verdict you believe is wrong is worth reporting: a suite that accuses the thing it tests is worse than no suite, because the real failures stop being believed.