Skip to content

endpoint WIT: no way to read the peer's application close (code, reason) — protocol refusals race their own connection close #48

Description

@lann

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions