Table of contents
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, orrate_limitedthrough packet-validation mapping; - duplicate claim takeover:
duplicate_session; - authority/transport effect failure:
internal_error; - maintenance and process lifecycle:
server_drainingfollowed, when necessary, byshutdown; - administrative session kick:
shutdownwithadmin_kickor 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:
timeoutmaps toTimedOuthealth;- 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_streakreports 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:
- Accepted stream fallback sequences produce streaks
1,2, then3. - At
3, the server sendsmove_input_datagram_degraded,true,3. - The client may switch subsequent movement to reliable stream and displays the advisory message as a transport notice.
- A later accepted primary datagram resets the server streak and emits
move_input_datagram_recovered,false,0. - 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
ServerDisconnectis terminal; aServerTransportAdvisoryis 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_streakcounts 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:
- Append a fresh numeric protobuf enum value; never edit an existing value.
- Add the Rust enum variant and one descriptor containing the stable name, wire value, and status label.
- Update
RealtimeDisconnectCode::ALL, producers, client presentation, smoke expectations, and round-trip tests. - Ship receiver support before any sender emits the value.
- 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:
- Define the stable public meaning and retry/operator implications.
- Extend the protobuf enum and descriptor table together.
- Emit through the typed helper at every relevant producer boundary.
- Update client state transition, status label, telemetry, and retry policy without parsing the message.
- Add codec tests for every registered code, an unknown-number rejection test, producer tests, and a client reducer test.
- Update registry capability/coverage metadata when packet availability changes and regenerate derived artifacts.
For an advisory addition:
- Define when the state transition begins, updates, and clears.
- Define field ownership and whether duplicate or reordered observations are safe.
- Add producer and consumer state-machine tests at threshold boundaries.
- 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_disconnectcaptured and client code recorded: typed terminal path;server_transport_advisoryfollowed 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.
Related Pages
- Realtime Protocol Registry
- Realtime Registry And Wire Contract
- Realtime Rollout Policy And Compatibility Negotiation
- QUIC Reliable Stream Topology And Negotiation
- Realtime Ingress Validation And Failure Policy
- Realtime Runtime Profile And Transport Adaptation
- Movement Authority
- Realtime Packet Debug Capture And Triage