Error Handling¶
The Signal Fish Client SDK uses two complementary error systems:
SignalFishError— a RustResult-based enum for errors returned by client methods (send failures, serialization issues, invalid state).ErrorCode— a protocol-level enum for structured error codes sent by the server inside events likeSignalFishEvent::ErrorandSignalFishEvent::AuthenticationError.
SignalFishError¶
All fallible client methods return Result<T>, which is an alias for
std::result::Result<T, SignalFishError>.
SignalFishError derives Debug and Error (via thiserror). It is an
exhaustive public enum:
| Variant | Fields | When it occurs |
|---|---|---|
TransportSend |
String |
Failed to send a message through the transport. |
TransportReceive |
String |
Failed to receive a message from the transport. |
TransportClosed |
— | The transport connection was closed unexpectedly. |
TokenBinding |
TokenBindingFailure |
Native WebSocket token-binding negotiation, challenge validation, key derivation, canonicalization, encoding, or sequence handling failed. Reasons are typed and contain no key, nonce, proof, signature, fingerprint, URL credential, or payload material. See WebSocket Token Binding. |
Serialization |
serde_json::Error |
Failed to serialize or deserialize a protocol message. Implements From<serde_json::Error>. |
NotConnected |
— | Attempted an operation requiring an active connection but the client is not connected. |
SendBufferFull |
capacity: usize |
The bounded outgoing command queue is full — the caller is producing messages faster than the transport can drain them. The message was refused, not queued; nothing is silently dropped. See Handling SendBufferFull. |
NotInRoom |
— | Attempted a room operation but the client is not in a room. |
AlreadyInRoom |
— | Attempted to join as a player/spectator or reconnect while already in a room. |
RoomOperationPending |
— | A previously admitted join, leave, or reconnect still awaits a matching typed terminal response. ping remains available; generic errors and absent responses stay fenced until transport teardown, after which a new connection may retry. |
WrongRoomRole |
required: RoomRole, actual: RoomRole |
Attempted a player-only command as a spectator, or leave_spectator as a player. |
AuthorityRequired |
— | Attempted start_game while another player is authority, or attempted to relinquish authority without currently holding it. |
ServerError |
message: String, error_code: Option<ErrorCode> |
The server returned an error message. |
ProtocolUnsupported |
mode: &'static str |
A protocol-v3-only operation (classified latest/volatile JSON, binary game data, signaling, or transport-status reporting) was attempted before v3 was negotiated. mode is "pre-negotiation" (no ProtocolInfo yet — negotiation still in flight) or "relay-only" (a ProtocolInfo arrived but negotiated v2, the terminal relay floor). See Protocol Versioning. |
SessionPlanUnavailable |
— | No authoritative WebRTC plan currently authorizes the signal: no plan has arrived, or the target is self, unknown, departed, or absent from the replace-on-plan peer set/current room roster. The set may be extended by a valid compatibility NewPeer; the frame is refused locally. |
StaleSessionGeneration |
attempted: Option<SessionGeneration>, current: Option<SessionGeneration> |
A generation-bound driver signal was produced after its session plan had been replaced. The client refuses it rather than relabeling stale signaling. |
BinaryFormatNotNegotiated |
— | A binary send was attempted after negotiation resolved to JSON. Request MessagePack and confirm effective_game_data_format() == Some(MessagePack); unsupported requests resolve to JSON and are refused before transport admission. Before ProtocolInfo, v3-only sends return ProtocolUnsupported instead. |
Timeout |
— | An operation timed out. |
Io |
std::io::Error |
An I/O error occurred. Implements From<std::io::Error>. |
Local validation has stable precedence: connection state, an admitted pending
room transition, membership/role, authority, protocol version, format/session
plan, and then bounded-queue capacity. Invalid state therefore does not consume
queue capacity and is not hidden by SendBufferFull.
Handling errors from client methods¶
use signal_fish_client::{
SignalFishClient, SignalFishConfig, SignalFishError, JoinRoomParams,
};
fn try_join(client: &mut SignalFishClient) {
let params = JoinRoomParams::new("my-game", "Alice");
match client.join_room(params) {
Ok(()) => println!("Join request sent"),
Err(SignalFishError::NotConnected) => {
eprintln!("Cannot join — not connected to the server");
}
Err(SignalFishError::TransportSend(msg)) => {
eprintln!("Transport send failed: {msg}");
}
Err(SignalFishError::Serialization(err)) => {
eprintln!("Serialization error: {err}");
}
Err(e) => {
eprintln!("Unexpected error: {e}");
}
}
}
The ? operator works naturally
Because SignalFishError implements std::error::Error, you can propagate
errors with ? in any function that returns Result<T, SignalFishError> or
a compatible error type.
ErrorCode¶
ErrorCode is a protocol-level enum with 53 variants representing
structured error codes returned by compatible Signal Fish servers. Server 0.7
currently emits 47 of them; six variants remain decodable for older servers and
are listed by ErrorCode::NON_EMITTED. It derives Debug,
Clone, PartialEq, Eq, Serialize, and Deserialize.
- Serializes as
SCREAMING_SNAKE_CASE(e.g.,"ROOM_NOT_FOUND") to match the server's JSON wire format. - Provides a
description()method returning a human-readable&'static str.
use signal_fish_client::ErrorCode;
let code = ErrorCode::RoomNotFound;
println!("{}", code.description());
// "The requested room could not be found. It may have been closed or the code is incorrect."
Authentication (11)¶
| Variant | Description |
|---|---|
Unauthorized |
Access denied. Authentication credentials are missing or invalid. |
InvalidToken |
The authentication token is invalid, malformed, or has expired. |
AuthenticationRequired |
This operation requires authentication. |
InvalidAppId |
The provided application ID is not recognized. |
AppIdExpired |
The application ID has expired. |
AppIdRevoked |
The application ID has been revoked. |
AppIdSuspended |
The application ID has been suspended. |
MissingAppId |
Application ID is required but was not provided. |
AuthenticationTimeout |
Authentication took too long to complete. |
SdkVersionUnsupported |
The SDK version you are using is no longer supported. |
UnsupportedGameDataFormat |
The requested game data format is not supported. |
Validation (6)¶
| Variant | Description |
|---|---|
InvalidInput |
The provided input is invalid or malformed. |
InvalidGameName |
The game name is invalid. |
InvalidRoomCode |
The room code is invalid or malformed. |
InvalidPlayerName |
The player name is invalid. |
InvalidMaxPlayers |
The maximum player count is invalid. |
MessageTooLarge |
The message size exceeds the maximum allowed limit. |
Room (7)¶
| Variant | Description |
|---|---|
RoomNotFound |
The requested room could not be found. |
RoomFull |
The room has reached its maximum player capacity. |
AlreadyInRoom |
You are already in a room. Leave the current room first. |
NotInRoom |
You are not currently in any room. |
RoomCreationFailed |
Failed to create the room. |
MaxRoomsPerGameExceeded |
The maximum number of rooms for this game has been reached. |
InvalidRoomState |
The room is in an invalid state for this operation. |
Authority (3)¶
| Variant | Description |
|---|---|
AuthorityNotSupported |
Authority features are not enabled on this server. |
AuthorityConflict |
Another client has already claimed authority. |
AuthorityDenied |
You do not have permission to claim authority in this room. |
Rate Limiting (2)¶
| Variant | Description |
|---|---|
RateLimitExceeded |
Too many requests in a short time. Slow down and try again later. |
TooManyConnections |
You have too many active connections. |
Reconnection (4)¶
| Variant | Description |
|---|---|
ReconnectionFailed |
Failed to reconnect to the room. |
ReconnectionTokenInvalid |
The reconnection token is invalid or malformed. |
ReconnectionExpired |
The reconnection window has expired. |
PlayerAlreadyConnected |
This player is already connected from another session. |
Spectator (4)¶
| Variant | Description |
|---|---|
SpectatorNotAllowed |
Spectator mode is not enabled for this room. |
TooManySpectators |
The room has reached its maximum spectator capacity. |
NotASpectator |
You are not a spectator in this room. |
SpectatorJoinFailed |
Failed to join as a spectator. |
Server (3)¶
| Variant | Description |
|---|---|
InternalError |
An internal server error occurred. |
StorageError |
A storage error occurred while processing your request. |
ServiceUnavailable |
The service is temporarily unavailable. |
Game Start — protocol v2 (2)¶
The game now starts explicitly via client.start_game() rather than
automatically when everyone is ready (see Concepts).
Applications migrating from readiness-based auto-start must call it after an
all_ready lobby update; see the 0.8 migration.
| Variant | Description |
|---|---|
GameStartNotReady |
Cannot start the game: not every player in the room is ready yet. |
GameStartForbidden |
You are not permitted to start the game. Only the room's authority may start it. |
Signaling — protocol v3 (5)¶
Returned only on a v3-negotiated connection, in response to a send_signal
(or send_offer / send_answer / send_ice_candidate / send_raw_signal)
that the server could not honor. See the Mesh Guide.
| Variant | Description |
|---|---|
CrossRoomSignal |
The signal targets a peer that is not in your room. |
UnsupportedTransport |
The requested data-path transport is not supported or was not negotiated for this connection. |
SignalTargetNotFound |
The signal's target peer could not be found in the room. |
SignalRateLimited |
Too many signaling messages were sent in a short time. Slow down and try again. |
SignalTooLarge |
The signal payload exceeds the maximum size allowed by the server. |
Connection Lifecycle — protocol v3 (1)¶
| Variant | Description |
|---|---|
ConnectionIdleTimeout |
The connection was closed by the server after being idle for too long. |
Delivery & Liveness (4)¶
| Variant | Description |
|---|---|
SlowConsumer |
The server evicted this connection because its outbound queue stayed full past the slow-consumer grace window (5 s by default): the client was not draining messages fast enough. The farewell frame carrying this code is written best-effort into an already-congested socket, so it may never arrive — a bare disconnect can be the only observable signal. |
ActivityTimeout |
The server closed the connection after prolonged protocol inactivity. Send periodic pings to keep the connection alive. |
ServerDraining |
The server is draining and will close the connection; preserve the current reconnect snapshot and honor retry guidance. |
InvalidDeliveryClass |
A classified GameData request used an invalid class/key shape or unsupported delivery token. Prefer the invalid-state-proof GameDataDelivery API. |
Protocol Negotiation (1)¶
| Variant | Description |
|---|---|
UnsupportedProtocolVersion |
The client's highest supported protocol version is below the server's configured minimum. |
The six new v3 server codes vs. SignalFishError::ProtocolUnsupported
The five Signal* codes plus ConnectionIdleTimeout are server-sent
ErrorCodes that arrive inside SignalFishEvent::Error. They are distinct
from the client-side SignalFishError::ProtocolUnsupported, which fails a
v3-only send locally before it ever reaches the server.
Error Handling Patterns¶
Handling SignalFishEvent::Error¶
The Error event is emitted when the server sends a generic error message.
It may include an ErrorCode for programmatic handling.
use signal_fish_client::{SignalFishEvent, ErrorCode};
match event {
SignalFishEvent::Error { message, error_code } => {
if let Some(code) = &error_code {
eprintln!("[{code}] {message}");
} else {
eprintln!("Server error: {message}");
}
}
_ => {}
}
Handling SignalFishEvent::AuthenticationError¶
AuthenticationError includes a non-optional ErrorCode. React to specific codes to
guide the user:
use signal_fish_client::{SignalFishEvent, ErrorCode};
match event {
SignalFishEvent::AuthenticationError { error, error_code } => {
match error_code {
ErrorCode::InvalidToken => {
eprintln!("Token expired or invalid — request a new token");
}
ErrorCode::InvalidAppId => {
eprintln!("Check your app ID configuration");
}
ErrorCode::SdkVersionUnsupported => {
eprintln!("Please upgrade to the latest SDK version");
}
_ => {
eprintln!("Authentication failed: {error}");
}
}
}
_ => {}
}
Retrying on RateLimitExceeded¶
When the server reports rate limiting, back off before retrying:
use signal_fish_client::{SignalFishEvent, ErrorCode};
use std::time::Duration;
async fn handle_event(event: SignalFishEvent) {
match event {
SignalFishEvent::Error { error_code, message } => {
if error_code == Some(ErrorCode::RateLimitExceeded) {
eprintln!("Rate limited: {message} — retrying after delay");
tokio::time::sleep(Duration::from_secs(2)).await;
// … retry the operation
}
}
_ => {}
}
}
Respect server rate limits
The RateLimitInfo provided in the Authenticated event tells you the
per-minute, per-hour, and per-day limits for your application. Proactively
throttling requests avoids RateLimitExceeded errors entirely.
Handling SendBufferFull¶
The synchronous send methods (send_game_data, send_signal, join_room, …)
fail fast with SignalFishError::SendBufferFull when the bounded outgoing
command queue (default 1024, set via
SignalFishConfig::command_channel_capacity) is full. The message is refused,
not queued — congestion is surfaced, never hidden. Three remedies, in order of
preference:
- Pace with the waiting variants.
send_game_data_reliable/send_signal_reliableare async and wait for a free slot instead of failing — the right tool for high-rate streams (rollback inputs, state sync). - Retry later. Treat the error as "try again next frame"; watch
send_capacity()to see the queue drain. - Raise the capacity.
SignalFishConfig::with_command_channel_capacity(n)buys more burst headroom, at the cost of more queued latency when the transport truly cannot keep up.
use signal_fish_client::{SignalFishClient, SignalFishError};
async fn stream_input(client: &mut SignalFishClient, input: serde_json::Value) {
match client.send_game_data(input.clone()) {
Ok(()) => {}
Err(SignalFishError::SendBufferFull { capacity }) => {
// `capacity` is the configured queue bound, not the current depth.
// Switch to the pacing variant instead of dropping the payload.
eprintln!("send queue full (configured capacity {capacity}); pacing");
if let Err(e) = client.send_game_data_reliable(input).await {
eprintln!("send failed: {e}");
}
}
Err(e) => eprintln!("send failed: {e}"),
}
}
Keep draining events while awaiting a reliable send
The command queue only drains while the transport loop runs, and the
loop pauses whenever the event channel is full (overflow pauses the loop
instead of dropping the event). A task that awaits
send_game_data_reliable while it is also
the only consumer of the event receiver can deadlock under simultaneous
send + receive pressure — drain events from a separate task. See the
send_game_data_reliable rustdoc
for details.
Distinguishing transport errors from server errors¶
Transport errors are returned by client methods via SignalFishError, while
server errors arrive asynchronously as SignalFishEvent variants. Handle both
layers for robust error recovery:
use signal_fish_client::{
SignalFishClient, SignalFishError, SignalFishEvent, ErrorCode,
};
fn send_data(client: &mut SignalFishClient) {
let payload = serde_json::json!({"action": "move", "x": 10, "y": 20});
match client.send_game_data(payload) {
Ok(()) => { /* sent successfully */ }
Err(SignalFishError::TransportSend(msg)) => {
eprintln!("Transport layer failed to send: {msg}");
}
Err(SignalFishError::TransportClosed) => {
eprintln!("Connection lost — need to reconnect");
}
Err(SignalFishError::NotConnected) => {
eprintln!("Client is not connected");
}
Err(SignalFishError::NotInRoom) => {
eprintln!("Must join a room before sending game data");
}
Err(e) => {
eprintln!("Send failed: {e}");
}
}
}
async fn handle_event(event: SignalFishEvent) {
match event {
// Server-side errors arrive as events
SignalFishEvent::Error { message, error_code } => {
match error_code {
Some(ErrorCode::MessageTooLarge) => {
eprintln!("Payload too large: {message}");
}
Some(ErrorCode::NotInRoom) => {
eprintln!("Server says we are not in a room");
}
Some(code) => {
eprintln!("Server error [{code}]: {message}");
}
None => {
eprintln!("Server error: {message}");
}
}
}
_ => {}
}
}
Two error channels
| Channel | Type | When |
|---|---|---|
Result<T> from client methods |
SignalFishError |
Immediate local failures (serialization, transport, invalid state). |
| Event receiver | SignalFishEvent::Error, AuthenticationError, RoomJoinFailed, etc. |
Asynchronous errors reported by the server. |