From wosh's M7 leg (our finding 23, lesson (a)), on post-#44 endpoints.
The race
A server-side protocol that answers "no" and hangs up — send a terminal Error frame on a stream, then connection.close(code, reason) — races itself: QUIC close discards in-flight stream data, so the refused client frequently observes plain error.closed ("stream closed") instead of the refusal it was sent. The refusal exists on the wire exactly once reliably: in CONNECTION_CLOSE's application close (code, reason). But the WIT only has the SEND side (connection.close(code, reason)); nothing lets the peer READ the code/reason after the fact.
Current workaround (works, awkward)
wosh's proxy sends the refusal frame and then deliberately does NOT close: it parks on the peer's close (wait-closed) so the client is guaranteed to drain the stream first, and only then tears down. That inverts the natural ownership (the refusing side should get to hang up) and costs a round trip of lifetime management on every refusal path.
Ask
An accessor for the peer's application close on connection — e.g.
/// After `state()` is `closed`: the peer's application close, when
/// one was received (code, reason). `none` for local/idle/transport
/// closes.
close-info: func() -> option<tuple<u32, string>>;
(or fold it into wait-closed's resolution). With that, refusal protocols collapse to "put the reason in the close" — no ordering dance, no discarded frames, and error.closed's diagnostics stop being lossy at exactly the moment they matter.
From wosh's M7 leg (our finding 23, lesson (a)), on post-#44 endpoints.
The race
A server-side protocol that answers "no" and hangs up — send a terminal
Errorframe on a stream, thenconnection.close(code, reason)— races itself: QUIC close discards in-flight stream data, so the refused client frequently observes plainerror.closed("stream closed") instead of the refusal it was sent. The refusal exists on the wire exactly once reliably: in CONNECTION_CLOSE's application close (code, reason). But the WIT only has the SEND side (connection.close(code, reason)); nothing lets the peer READ the code/reason after the fact.Current workaround (works, awkward)
wosh's proxy sends the refusal frame and then deliberately does NOT close: it parks on the peer's close (
wait-closed) so the client is guaranteed to drain the stream first, and only then tears down. That inverts the natural ownership (the refusing side should get to hang up) and costs a round trip of lifetime management on every refusal path.Ask
An accessor for the peer's application close on
connection— e.g.(or fold it into
wait-closed's resolution). With that, refusal protocols collapse to "put the reason in the close" — no ordering dance, no discarded frames, anderror.closed's diagnostics stop being lossy at exactly the moment they matter.