alt-svc: h3=":443" is an advertisement, not a completed HTTP/3 connection. It says that a client may try the origin over HTTP/3 at UDP port 443. A useful acceptance check must also prove that the client supports HTTP/3, QUIC reaches the selected endpoint, TLS and HTTP negotiation finish, and the response arrives without an older HTTP version rescuing the request.
This guide is for operators, developers, and hosting teams that can run Bash from an authorized Linux host. It assumes ordinary curl familiarity, then explains the HTTP/3-specific boundaries. The reproduced path makes read-only requests to Cloudflare’s public QUIC test endpoint and creates one local TCP-only TLS fixture; it changes no public DNS, CDN, firewall, or web-server configuration.
HTTP/3 carries HTTP over QUIC, and QUIC uses UDP rather than TCP. RFC 9114 defines that protocol mapping. Consequently, a successful HTTP/2 request and a valid Alt-Svc header can coexist with a broken UDP/443 path. The distinction matters during a CDN rollout, firewall change, load-balancer reload, or regional routing incident.
Alt-Svc is a discovery mechanism. The server can advertise another protocol, host, and port on an HTTP/1.1 or HTTP/2 response. HTTP/3 Explained’s Alt-Svc chapter describes the header as advice to attempt that alternate service; the client still has to establish it.
Three states therefore deserve separate evidence:
h3 Alt-Svc value.Neither admission nor resilience substitutes for the other. Strict admission finds a broken HTTP/3 path. Fallback keeps visitors working on networks that block or throttle UDP, but that same behavior can hide the protocol failure from a generic uptime check.
Start by declaring one endpoint and a collision-safe receipt directory. Debian’s packaged curl is used here; other distributions may ship a build without HTTP/3. Refuse a pre-existing path instead of erasing unknown files.
set -euo pipefail
LAB=/tmp/voxfor-http3-156
ENDPOINT=https://cloudflare-quic.com/
if [[ -e "$LAB" ]]; then
printf 'Refusing pre-existing lab path: %s\n' "$LAB" >&2
exit 9
fi
install -d -m 0700 "$LAB"
printf 'scope=voxfor-http3-156\nendpoint=%s\n' "$ENDPOINT" > "$LAB/owner"
curl -V | tee "$LAB/curl-version.txt"
grep -Eq '^Features:.*HTTP3' "$LAB/curl-version.txt"
printf 'client_http3_feature=yes\n'
Client admission begins with the Features: line. Seeing nghttp3 in a library list is useful context, but the explicit HTTP3 feature is clearer. Current curl HTTP/3 documentation also distinguishes --http3, which may race an older version, from --http3-only, which does not.
Fetch the headers explicitly over HTTP/2. This makes the discovery layer visible while preventing the command itself from becoming accidental HTTP/3 proof.
set -euo pipefail
LAB=/tmp/voxfor-http3-156
ENDPOINT=https://cloudflare-quic.com/
grep -qx 'scope=voxfor-http3-156' "$LAB/owner"
curl --http2 --silent --show-error --head "$ENDPOINT" > "$LAB/http2-headers.txt"
grep -Ei '^(HTTP/2|alt-svc:)' "$LAB/http2-headers.txt" | tee "$LAB/advertisement.txt"
grep -Eiq '^alt-svc:.*h3=' "$LAB/http2-headers.txt"
printf 'advertises_h3=yes\n'
Our observed response contained HTTP/2 200 and alt-svc: h3=":443"; ma=86400. The ma parameter is the advertisement’s maximum age in seconds, not a promise that every path will work for that period. Cached discovery data can outlive a rollback or edge change, so retain the exact value and observation time during incident work.
Header ownership can also sit at a different layer from the application. A CDN or reverse proxy may add Alt-Svc while the origin knows nothing about QUIC. Conversely, a direct origin test can bypass the edge that visitors actually use. Keep hostname, resolved address, network path, and delivery layer in the receipt.
curl --http3 is designed for resilient application use: it can start an older HTTP attempt when QUIC is slow or fails. For protocol acceptance, use --http3-only and record curl’s negotiated version rather than trusting the header text returned by the server.
set -euo pipefail
LAB=/tmp/voxfor-http3-156
ENDPOINT=https://cloudflare-quic.com/
grep -qx 'scope=voxfor-http3-156' "$LAB/owner"
curl --http3-only --max-time 10 --silent --show-error --output /dev/null \
--write-out 'strict_http_version=%{http_version} strict_code=%{response_code} strict_remote_ip=%{remote_ip}\n' \
"$ENDPOINT" | tee "$LAB/strict.txt"
grep -Eq '^strict_http_version=3 strict_code=200 ' "$LAB/strict.txt"
Two checks are deliberate. http_version=3 proves the completed transfer used HTTP/3; response_code=200 proves the application returned the expected status. A QUIC handshake that ends in a redirect, authentication error, or wrong virtual host may establish the protocol while still failing the reader’s real task.
remote_ip is evidence, not a permanent allowlist. Anycast CDNs can return another edge from another resolver, address family, location, or moment. When the endpoint represents your own deployment, add an application-specific response assertion such as a stable health token, release ID, or content hash. Do not treat transport success as proof that checkout, login, uploads, or other customer journeys work.
A hostname may resolve to both address families while only one admits QUIC. Firewalls, peering, CDN configuration, MTU behavior, and local network policy can differ. Run both strict transfers when the monitoring location has real connectivity for each family.
set -euo pipefail
LAB=/tmp/voxfor-http3-156
ENDPOINT=https://cloudflare-quic.com/
grep -qx 'scope=voxfor-http3-156' "$LAB/owner"
for family in 4 6; do
curl -"$family" --http3-only --max-time 10 --silent --show-error --output /dev/null \
--write-out "family=ipv$family http_version=%{http_version} response_code=%{response_code} remote_ip=%{remote_ip}\\n" \
"$ENDPOINT"
done | tee "$LAB/families.txt"
grep -Eq '^family=ipv4 http_version=3 response_code=200 ' "$LAB/families.txt"
grep -Eq '^family=ipv6 http_version=3 response_code=200 ' "$LAB/families.txt"
From this reproduced host, both IPv4 and IPv6 completed HTTP/3 200. That result is intentionally path-specific. A monitor in one data center cannot prove admission from an office network, mobile carrier, customer region, or VPN. Use the same route-and-time discipline described in Voxfor’s repeatable network path testing: label source, destination, direction, address family, time, and repeated outcome.
If IPv6 is not configured on the test host, record not_tested_no_ipv6_path; do not silently count it as a failure or a pass. If IPv6 exists but the strict transfer fails, preserve verbose curl output and compare DNS, route, UDP/443 policy, certificate identity, edge selection, and packet loss before changing the HTTP application.
--http3 Can Hide FailureA positive strict request proves the selected public path now, but it does not demonstrate the difference between the two curl modes. The following negative control creates a local TLS server that listens on TCP only. There is no UDP listener and no HTTP/3 service.
Because the certificate is self-signed, --insecure is used only against loopback for this disposable fixture. Never carry that option into a production endpoint check; certificate identity is part of QUIC admission.
set -euo pipefail
LAB=/tmp/voxfor-http3-156
grep -qx 'scope=voxfor-http3-156' "$LAB/owner"
if ss -H -ltn 'sport = :18443' | grep -q .; then
printf 'TCP port 18443 is already in use\n' >&2
exit 10
fi
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout "$LAB/local.key" -out "$LAB/local.crt" \
-subj /CN=localhost -days 1 >/dev/null 2>&1
printf 'tcp fallback fixture\n' > "$LAB/index.html"
( cd "$LAB" && exec openssl s_server -quiet -accept 127.0.0.1:18443 \
-cert local.crt -key local.key -WWW >server.log 2>&1 ) &
server_pid=$!
printf '%s\n' "$server_pid" > "$LAB/server.pid"
for attempt in $(seq 1 30); do
timeout 0.1 bash -c '>/dev/tcp/127.0.0.1/18443' 2>/dev/null && break
sleep 0.05
done
kill -0 "$server_pid"
ps -p "$server_pid" -o args= | grep -F 'openssl s_server'
fallback_result=$(curl --insecure --http3 --max-time 5 --silent --show-error \
--output /dev/null --write-out 'version=%{http_version} code=%{response_code}' \
https://127.0.0.1:18443/index.html)
set +e
strict_result=$(curl --insecure --http3-only --max-time 2 --silent --show-error \
--output /dev/null --write-out 'version=%{http_version} code=%{response_code}' \
https://127.0.0.1:18443/index.html 2>&1)
strict_rc=$?
set -e
printf 'fallback_rc=0 %s\nstrict_rc=%s %s\n' "$fallback_result" "$strict_rc" "$strict_result" \
| tee "$LAB/fallback-control.txt"
printf '%s\n' "$fallback_result" | grep -Eq '^version=(1|1\.1|2) code=200$'
test "$strict_rc" -ne 0
printf '%s\n' "$strict_result" | grep -Eq 'version=0 code=000'
Ordinary --http3 returned an HTTP 200 over an older version, while --http3-only failed. That is the exact false-green risk: availability passed, HTTP/3 admission did not. The Everything curl HTTP/3 chapter explains that the fallback-capable mode starts an older transfer attempt when QUIC is unavailable or too slow.
Use both monitor types when the deployment promise needs both. A customer-journey monitor should allow fallback because real browsers need the page to work. A separate strict probe should alert when the HTTP/3 path disappears. Voxfor’s customer-journey uptime method helps define the first signal; strict curl adds the protocol-specific signal.
Assemble a short receipt only after every assertion succeeds. The hash makes later edits detectable; it is not a signature and does not establish who ran the test.
set -euo pipefail
LAB=/tmp/voxfor-http3-156
grep -qx 'scope=voxfor-http3-156' "$LAB/owner"
{
grep -E '^curl ' "$LAB/curl-version.txt" | head -1
cat "$LAB/advertisement.txt"
cat "$LAB/strict.txt"
cat "$LAB/families.txt"
cat "$LAB/fallback-control.txt"
} > "$LAB/receipt.txt"
grep -Eq '^strict_http_version=3 strict_code=200 ' "$LAB/receipt.txt"
test "$(grep -c 'http_version=3 response_code=200' "$LAB/receipt.txt")" -eq 2
grep -Eq '^fallback_rc=0 version=(1|1\.1|2) code=200$' "$LAB/receipt.txt"
grep -Eq '^strict_rc=[1-9][0-9]* ' "$LAB/receipt.txt"
sha256sum "$LAB/receipt.txt" | tee "$LAB/receipt.sha256"
printf 'receipt_complete=yes\n'
cat "$LAB/receipt.txt"
For a real change, record UTC time, source location, curl version/backend, hostname, address family, remote IP, Alt-Svc value, strict protocol and status, application assertion, and change/release identity. Repeat from representative networks after caches and edge configuration converge. A single successful edge does not prove a global CDN footprint.
Monitoring should preserve the same distinction. A normal HTTP probe may continue through HTTP/2 and remain healthy. A strict shell probe can run beside a layered Prometheus Blackbox Exporter check rather than pretending that ordinary HTTP/TLS status already proves QUIC. HTTP/3 also does not reduce origin processing time; use NGINX request and upstream timing when the transport passes but responses remain slow.
HTTP3 curl feature: the client cannot test the protocol; install a trusted compatible build before diagnosing the server.During a proxy change, keep an older configuration ready and repeat live traffic checks across the protocol boundary. Voxfor’s HAProxy graceful reload test illustrates why new-request convergence and old-connection behavior need observable evidence; QUIC listener ownership remains a separate check.
h3 header prove HTTP/3 works?No. It proves that the response advertised an HTTP/3 alternative. The client must still reach the advertised UDP service, complete QUIC and TLS negotiation, issue the HTTP request, and receive the expected application result.
--http3-only instead of --http3?--http3-only fails when curl cannot establish HTTP/3. Ordinary --http3 can start HTTP/2 or HTTP/1.1 in parallel or after a QUIC failure, so a successful request may be availability evidence without being HTTP/3 evidence.
Use two monitors for two promises. Keep fallback for a real customer journey, because visitors need the service to work on networks that block QUIC. Add a separate strict HTTP/3 probe when losing that protocol is itself actionable.
No. It validates one resolved address, source network, address family, edge, certificate, response, and observation time. Repeat from representative regions and both IP families when those paths are part of the delivery promise.
No. It proves transport admission and the asserted response. Application execution, cache policy, object weight, database work, origin latency, and user-device rendering can still dominate performance.
Save the time, source location, curl build, hostname, resolved address, Alt-Svc value, strict HTTP version, status, application assertion, address-family results, negative-control behavior, and release identity. Redact private topology before sharing the receipt externally.
The checked path is accepted when the curl build explicitly supports HTTP/3, the older-version response advertises h3, a strict request completes with http_version=3 and the expected application status, every required address family passes independently, and the TCP-only control succeeds only through fallback while strict mode fails. A missing layer is an incomplete result, not a partial HTTP/3 pass.
Stop only the marker-owned process and remove only the exact lab directory. PID identity is checked before signaling it.
set -euo pipefail
LAB=/tmp/voxfor-http3-156
grep -qx 'scope=voxfor-http3-156' "$LAB/owner"
server_pid=$(<"$LAB/server.pid")
ps -p "$server_pid" -o args= | grep -F 'openssl s_server'
kill "$server_pid"
wait "$server_pid" 2>/dev/null || true
find "$LAB" -xdev -depth -delete
test ! -e "$LAB"
printf 'cleanup_scope=%s absent=yes\n' "$LAB"
client_http3_feature=yes
HTTP/2 200
alt-svc: h3=":443"; ma=86400
advertises_h3=yes
strict_http_version=3 strict_code=200 strict_remote_ip=2606:4700::6812:1b0e
family=ipv4 http_version=3 response_code=200 remote_ip=104.18.26.14
family=ipv6 http_version=3 response_code=200 remote_ip=2606:4700::6812:1a0e
fallback_rc=0 version=1 code=200
strict_rc=7 curl: (7) ... Connection refused
version=0 code=000
receipt_complete=yes
cleanup_scope=/tmp/voxfor-http3-156 absent=yes
Local rollback stops only the OpenSSL process whose saved PID still identifies openssl s_server, then deletes /tmp/voxfor-http3-156 after its exact owner marker matches. For a real rollout failure, restore the last known-good CDN, listener, firewall, DNS, or load-balancer configuration through its own change record; do not disable certificate verification, open UDP broadly, clear unrelated caches, or remove HTTP/2 fallback merely to make the strict probe green.