WebTransport Echo

Exhibit · WebKit and Network.framework

An unexplained Safari refusal

Two servers on this host present the same advertised values, as far as they have been compared, and Safari on iOS 27.0 beta establishes a session against one while refusing the other. Chrome and Firefox accept both. This page is a live reproduction and the wire evidence, published so that anyone with better visibility into WebKit can identify what is missing from it.

Observed on iOS 27.0 beta only, six attempts for six, on two devices across two networks. Not confirmed against a release build, and not yet reproduced by anyone outside this project.

The pair

Run this on an iPhone or iPad to reproduce. On a desktop browser both go green, which is the control.

What the server observes on a refusal

QUIC and TLS complete. SETTINGS are exchanged in both directions. The client opens the extended CONNECT stream, writes zero bytes, and abandons it.

// server qlog, refusal case
transport:packet_received     handshake complete
http:frame_parsed             SETTINGS  (nine identifiers, both directions)
http:stream_opened            stream 0, extended CONNECT
transport:frame_received      stop_sending(stream 0, error 0x10C)   // H3_REQUEST_CANCELLED
transport:frame_received      reset_stream(stream 0, final_size 0)
transport:connection_closed   0x100                              // H3_NO_ERROR, empty reason

final_size = 0 is the tell: the CONNECT stream was created and nothing was ever written to it. The decision was taken on the SETTINGS exchange.

Client-side the same moment is a WebTransportError with an empty message, roughly 50 ms after construction.

What differs between the two, and what does not

:4440:4436
h3 SETTINGSNine identifiers, matching on every value compared
QUIC transport parametersMatching on every value compared
CertificateThe same Let's Encrypt chain
Echo handlerThe same code
Server buildwebtransport-go v0.12.0 · quic-go v0.61.0webtransport-go v0.11.0 · quic-go v0.60.0
iOS 27.0 betarefusedbinds
Chrome, Firefoxbindsbinds

If that comparison is complete, then what Safari reacts to is not an advertised value at all, which would put it in frame encoding, capsule handling or sequencing. That inference is worth exactly as much as the comparison behind it, and the comparison was made by reading decoded values rather than by diffing raw frames. The captures are published below so the inference can be checked rather than taken on trust.

Where this is weakest. Two dependencies move between those builds, webtransport-go and quic-go together, so this is not a one-variable comparison. Pinning webtransport-go v0.11.0 against quic-go v0.61.0 as a fifth endpoint would separate them. Until that endpoint exists, neither library can be singled out, and an interaction between the two cannot be ruled out.


A second, separable question

iOS 27 also enforces the draft-13 conditional that WT_MAX_SESSIONS > 1 obliges a server to send the three WT_INITIAL_MAX_* settings. A server advertising the former without the latter is refused before CONNECT.

This is defensible behavior and no other engine enforces it. Worth confirming that the enforcement is intended, since it makes several widely deployed server defaults unreachable from Safari with no diagnostic.

The reporting problem underneath both

Invalid settings, TLS rejection and the wire-level refusal above all surface to web developers as the same WebTransportError with an empty message at the same latency. There is no client-side way to tell them apart, so the failures are only diagnosable by whoever controls the server.

Related: the server-library side of the same pair · what this means if you are building on WebTransport