1 typed disconnect codes and transport advisories
forgejo-actions edited this page 2026-07-15 12:43:13 +00:00

Typed Disconnect Codes And Transport Advisories

Purpose

Typed disconnects give clients and operators a stable machine-readable terminal reason. Transport advisories report a recoverable path condition while the realtime session remains active. They share the reliable control stream and the typed_disconnects_v1 capability, but they have different lifecycle semantics and must never be treated as interchangeable status messages.

This page explains the contract and usage. Packet IDs, budgets, and generated coverage remain in the Realtime Protocol Registry.

Protocol Boundaries

Signal Direction Payload Lifecycle effect
ClientDisconnect Client to server Empty control packet Requests clean stream termination. It carries no client reason code and is never replayed on reconnect.
ServerDisconnect Server to client Disconnect { code, message } Terminal. The client closes the authority phase, clears prediction and snapshot staging, and records the stable code.
ServerTransportAdvisory Server to client TransportAdvisory Non-terminal. It updates movement transport recovery state and may move future input onto reliable stream fallback.
HTTP game handshake rejection Server to client GameHandshakeResponse or API error Pre-realtime admission result, not a protobuf Disconnect.
QUIC/stream close without ServerDisconnect Either Transport close/error Untyped transport loss. Do not invent a remote disconnect code from the local error text.

All three realtime packets use the reliable control stream. Reliable delivery orders them within that stream, but a process or connection can disappear before a queued terminal packet is observed. Absence of a captured server_disconnect is therefore not evidence for a particular typed cause.

Stable Disconnect Code Contract

The protobuf enum number and snake-case stable name identify the same condition. The Rust descriptor table is the conversion boundary used by encoding, decoding, display, parsing, and UI status labels.

Wire value Stable name Meaning
0 no stable name DISCONNECT_CODE_UNSPECIFIED; decodes to None and exists for untyped/legacy payloads
1 protocol_version_mismatch The peer protocol generation or negotiated version is incompatible
2 invalid_handoff_token The realtime session binding token is missing, invalid, consumed, or otherwise not admissible
3 handoff_expired A valid handoff lifetime ended before activation
4 authentication_failed Authentication or trusted identity validation failed
5 realm_unavailable The requested realm cannot accept the session
6 zone_unavailable The requested zone or owning zone authority is unavailable
7 server_full Server admission capacity is exhausted
8 server_draining The server entered maintenance drain and is asking sessions to leave
9 rate_limited Ingress abuse or fairness policy requires terminal throttling
10 malformed_packet The peer sent an invalid envelope, payload, direction, enum, or field shape
11 packet_too_large A frame, payload, or validated field exceeded its protocol budget
12 timeout The session exceeded a liveness deadline for which a typed terminal notice is available
13 duplicate_session A newer claim replaced this active character session
14 transfer_failed A session or zone transfer could not complete safely
15 shutdown The service is terminating or an administrative close uses shutdown semantics
16 internal_error An internal authority or transport effect failed without a more specific public code

The enum is closed. A decoder rejects an unknown numeric value as InvalidDisconnectCode; it does not silently map the value to internal_error. Code zero is the only defined no-code state.

The human-readable message is diagnostic detail, not an alternate machine code. Production encoding substitutes the stable code name when the caller supplies an empty message. Consumers should branch on the typed code, then show or log the message as context.

Producers

The game server centralizes active-session emission in send_typed_disconnect, with direct stream-handshake writes using the same encoder. Current producers include:

  • invalid realtime hello/session binding: invalid_handoff_token;
  • malformed, oversized, and terminal rate-limited ingress: malformed_packet, packet_too_large, or rate_limited through packet-validation mapping;
  • duplicate claim takeover: duplicate_session;
  • authority/transport effect failure: internal_error;
  • maintenance and process lifecycle: server_draining followed, when necessary, by shutdown;
  • administrative session kick: shutdown with admin_kick or the supplied operator message.

The stable vocabulary also reserves codes for compatibility, authentication, realm/zone admission, expiry, timeout, transfer failure, and capacity. A code being defined does not imply that every pre-realtime rejection currently emits a ServerDisconnect. In particular, HTTP handoff negotiation occurs before the QUIC realtime stream exists and returns its own admission response.

Clients produce only the empty ClientDisconnect. The server treats it as a request to stop processing the stream and enter cleanup; clients must not expect an echoing server reason.

Client Consumption

The common-proto decoder converts a recognized wire enum to its stable snake-case name. The multiplayer reducer stores that optional code and transitions the session to a terminal state:

  • timeout maps to TimedOut health;
  • every other recognized code and an unspecified code map to DisconnectedByServer;
  • prediction queues, snapshot staging, selected authoritative projections, and transient transport notices are cleared or marked stale;
  • the UI obtains a stable status label from the same descriptor table and keeps distinct message detail when present.

Do not parse message to recover a code. Do not treat a local reconnect-grace timeout, QUIC idle timeout, EOF, or connection reset as though the server sent timeout unless a decoded ServerDisconnect actually carried that code.

Transport Advisory Contract

TransportAdvisory.code is an extensible stable string, not the closed disconnect enum. The current movement transport state machine defines:

  • move_input_datagram_degraded: move_input_datagram_degraded = true; fallback_streak reports consecutive accepted fallback deliveries.
  • move_input_datagram_recovered: move_input_datagram_degraded = false; fallback_streak = 0; the server has accepted the primary movement transport after a degraded period.

The default negotiated degradation threshold is three fallback wins. The server increments the streak only when it accepts movement on the configured fallback transport. An accepted primary-transport input resets the streak. Crossing the threshold emits one degraded advisory; further fallback inputs do not repeat it until primary recovery emits one recovered advisory.

On the client, the structured boolean and streak drive the shared transport state machine. A degraded advisory at or above the negotiated threshold can switch future movement input from datagram primary to reliable stream and remove the fallback lane from the effective client profile. A recovered advisory clears degraded recovery state and the persistent transport notice. It does not acknowledge a movement sequence; only a movement correction or snapshot acknowledgement does that.

The code and message are retained for diagnostics, while current behavioral reduction is based on move_input_datagram_degraded and fallback_streak. Unknown advisory strings decode successfully. New advisory behavior must therefore have explicit structured semantics rather than relying on clients to parse prose.

Local client fallback can begin before a server advisory arrives. The advisory reports the server's accepted-transport observation and synchronizes recovery state; it is not the sole trigger for client-side fallback deadlines.

Examples

Terminal validation failure

A client sends a control envelope above its budget. The server maps the validation failure to wire enum 11, sends stable code packet_too_large with budget detail in message, and stops the affected stream according to ingress policy. The client branches on packet_too_large; it does not match the budget wording.

Degrade And Recover

A datagram-primary session has stream fallback and threshold 3:

  1. Accepted stream fallback sequences produce streaks 1, 2, then 3.
  2. At 3, the server sends move_input_datagram_degraded, true, 3.
  3. The client may switch subsequent movement to reliable stream and displays the advisory message as a transport notice.
  4. A later accepted primary datagram resets the server streak and emits move_input_datagram_recovered, false, 0.
  5. Movement acknowledgement remains governed by acknowledged_sequence, independently of both advisories.

Clean Client Close

The client queues the empty disconnect control packet, flushes it best-effort, and closes QUIC with local text client disconnected. The server exits the realtime ingress loop. No typed client code is present and no server disconnect echo is required.

Invariants

  • A ServerDisconnect is terminal; a ServerTransportAdvisory is not.
  • Disconnect logic branches on the enum/stable code, never on message.
  • Existing nonzero enum numbers and stable names are permanent and must not be removed, renumbered, or reused.
  • Unknown disconnect enum values fail decoding. Unspecified zero remains distinguishable from every typed code.
  • Advisory codes are open strings, but machine behavior must be represented by compatible structured fields.
  • fallback_streak counts accepted fallback transport observations, not packet loss, retries, elapsed time, or pending movement depth.
  • A recovery advisory clears degradation state but is not a movement acknowledgement.
  • Client disconnect has no reason payload and is never replayed after reconnect.
  • Cosmetic datagram throttling may drop and continue when registry ingress policy says so; only terminal policy emits a typed disconnect.
  • A transport close without a decoded terminal packet has no authoritative server disconnect code.

Compatibility Rules

typed_disconnects_v1 is stable, default-enabled, and required by the current compatibility policy. It gates client disconnect, server disconnect, and server transport advisory packets.

Adding a disconnect code is stricter than adding an advisory string because older decoders reject unknown enum values:

  1. Append a fresh numeric protobuf enum value; never edit an existing value.
  2. Add the Rust enum variant and one descriptor containing the stable name, wire value, and status label.
  3. Update RealtimeDisconnectCode::ALL, producers, client presentation, smoke expectations, and round-trip tests.
  4. Ship receiver support before any sender emits the value.
  5. Add a capability or protocol-version boundary if any supported receiver can encounter the new value before it upgrades.

For a new advisory, preserve existing field meanings and use fresh optional protobuf tags for additive data. If old clients cannot safely ignore the new behavior, add a capability or a new packet contract. A new code string alone is not a compatibility mechanism.

Do not create another hand-maintained code registry. The protobuf enum and common-proto descriptor table own numeric/string conversion; the generated realtime registry owns packet availability, delivery, capability, and coverage metadata.

Extension Workflow

For any terminal condition, first decide whether it is truly session-ending. Prefer an advisory, drop-and-continue ingress policy, or recoverable transfer result when the session can continue safely.

For a terminal addition:

  1. Define the stable public meaning and retry/operator implications.
  2. Extend the protobuf enum and descriptor table together.
  3. Emit through the typed helper at every relevant producer boundary.
  4. Update client state transition, status label, telemetry, and retry policy without parsing the message.
  5. Add codec tests for every registered code, an unknown-number rejection test, producer tests, and a client reducer test.
  6. Update registry capability/coverage metadata when packet availability changes and regenerate derived artifacts.

For an advisory addition:

  1. Define when the state transition begins, updates, and clears.
  2. Define field ownership and whether duplicate or reordered observations are safe.
  3. Add producer and consumer state-machine tests at threshold boundaries.
  4. Keep the session live and keep acknowledgements in their owning protocol.

When registry metadata changes, verify it with:

cargo run -p aether-common-proto --bin aether-realtime-registry -- --check

Focused contract tests include:

cargo test -p aether-common-proto disconnect_packet_codec_round_trips_every_registered_code
cargo test -p aether-common-proto disconnect_decode_rejects_unknown_disconnect_code
cargo test -p aether-game-server accepted_move_transport_tracks_fallback_then_primary_recovery
cargo test -p aether apply_disconnect_packet_maps_timeout_code_to_timed_out_health

Debugging And Packet Capture

Enable metadata-only capture before starting the normal process:

$env:AETHER_PACKET_DEBUG_CAPTURE_DIR = "artifacts/packet-capture"

Filter terminal and advisory control packets after reproducing:

Get-Content artifacts/packet-capture/*.jsonl |
  ForEach-Object { $_ | ConvertFrom-Json } |
  Where-Object { $_.packet -in @("client_disconnect", "server_disconnect", "server_transport_advisory") } |
  Select-Object timestamp_unix_ms, executable, direction, packet, message_id, protocol_major, protocol_minor, payload_len

Capture stores packet metadata, not protobuf payload bytes. It can establish that a named control packet was observed, its direction, protocol envelope, and size; it cannot reveal the disconnect enum, message, advisory code, degradation boolean, or fallback streak. Pair it with server validation/admission logs, movement transport metrics, client last_disconnect_code, connection health, transport notice, and shutdown/drain logs.

During triage, distinguish these cases explicitly:

  • no server_disconnect, followed by EOF or QUIC close: untyped transport termination;
  • server_disconnect captured and client code recorded: typed terminal path;
  • server_transport_advisory followed by live traffic: expected non-terminal adaptation;
  • HTTP handshake rejection before realtime dial: admission path outside this wire payload;
  • advisory captured but no movement transport change: compare the structured streak with the negotiated threshold and inspect movement acknowledgements.

Packet-capture timestamps are local wall-clock observations. Use stream order and application logs for causality, especially when comparing files from different hosts.