§2.5
Chapter 02 · QUIC Connection Establishment and Security Handshake

§2.5Debugging Handshake Failures with Trace Interpretation

RFC 9001RFC 9000

Learning objective. Turn a failed handshake into a diagnosis: recognize how failures surface in a trace, decode the error code, and map the common symptoms to their causes using real captured output.

Failures speak through CONNECTION_CLOSE #

A handshake that fails does not hang silently forever: one side gives up and sends a CONNECTION_CLOSE frame carrying an error code (§5.4). Reading that code is most of the diagnosis. Two families cover nearly all handshake failures:

  • Transport error codes (0x000x10, constants.md §7). The QUIC layer itself objected: a malformed frame (FRAME_ENCODING_ERROR), a bad transport parameter (TRANSPORT_PARAMETER_ERROR), a protocol violation.
  • CRYPTO_ERROR (0x01000x01ff). TLS objected. The low byte is the TLS AlertDescription: a TLS alert is turned into a QUIC error by adding it to 0x0100 ([RFC 9001 §4.8]). So 0x0100 + alert tells you exactly which TLS check failed.
ServerClienta TLS check fails on one sidealt[server rejects (e.g. nocommon ALPN)][client rejects (e.g. badcertificate)]never reaches 1-RTT · decode: 0x01xx⇒ TLS alert (xx)Initial [CRYPTO: ClientHello]Initial [CRYPTO: ServerHello] ·Handshake [CRYPTO: Certificate…]CONNECTION_CLOSE[CRYPTO_ERROR 0x128]CONNECTION_CLOSE[CRYPTO_ERROR 0x12a]
Fig. 2.5-1Either side can abort the handshake with a CRYPTO_ERROR before 1-RTT keys exist. The low byte of the code is the TLS alert that triggered it.RFC 9001 §4.8

Reading real failures #

The §2.1 capture harness makes failures reproducible. The lab in labs/02-5-handshake-failures/ points a client at a server it cannot complete a handshake with (once with a mismatched ALPN, once verifying the server's self-signed certificate) and reads the CONNECTION_CLOSE error out of the client's qlog. This is its genuine output:

captured output — uv run labs/02-5-handshake-failures/failures.pyTEXT
server: ALPN=['h3'], self-signed cert

ALPN mismatch   : error 0x128  (CRYPTO_ERROR · TLS alert 40 = handshake_failure)
                  reason: "No common ALPN protocols"
Cert not trusted: error 0x12a  (CRYPTO_ERROR · TLS alert 42 = bad_certificate)
                  reason: "hostname '127.0.0.1' doesn't match DNSPattern(...)"

Both codes are in the CRYPTO_ERROR range, so subtract 0x0100: 0x128 → 40 and 0x12a → 42, the TLS alerts handshake_failure and bad_certificate. The cert failure is textbook. The client rejected the server's certificate, so the client sent the close. The ALPN case is a useful subtlety: this stack rejected the missing common protocol at the TLS layer with handshake_failure (40), whereas an HTTP/3 endpoint that negotiates ALPN at its own layer would use no_application_protocol (0x0178, [RFC 9114 §8.1]). The lesson is to decode the alert number rather than assume it: implementations differ on which alert a given failure earns.

A field guide to handshake symptoms #

Symptom in the trace Likely cause
CRYPTO_ERROR 0x012a/0x0130 (bad_certificate / certificate_unknown) server cert not trusted, wrong name, or expired
CRYPTO_ERROR 0x0128 or 0x0178 (handshake_failure / no_application_protocol) ALPN mismatch — client and server share no protocol
TRANSPORT_PARAMETER_ERROR (0x08) missing/invalid transport parameter, or a 0-RTT limit reduced (§2.3)
Version Negotiation packet returned, no handshake client's QUIC version unsupported (§7.4)
Client Initial sent, nothing back at all first datagram below the 1200-byte floor, or UDP blocked (§1.4)
Server stops after one flight, then a long pause 3× anti-amplification limit reached; server waiting for a client packet (§8.1)

Takeaways #

Most handshake debugging reduces to one move: find the CONNECTION_CLOSE, read its error code, and (if it is in the 0x01xx range) subtract 0x0100 to recover the TLS alert. The silent failures (no response, or a stall after one flight) are the exceptions, and they trace to datagram size or the anti-amplification limit rather than to TLS. That completes the handshake chapter; Chapter 3 turns to what happens after the connection is up: how QUIC detects and recovers from loss.