Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

API Errors

Errors produced by aviso’s own handlers use a consistent JSON error object on 4xx and 5xx responses. A small number of 4xx responses are framework-level fallbacks rather than aviso-handled errors and use a different shape; see Framework-Level Fallbacks below.

Response Shape

{
  "code": "INVALID_REPLAY_REQUEST",
  "details": "Replay endpoint requires either from_id or from_date parameter...",
  "error": "Invalid Replay Request",
  "message": "Replay endpoint requires either from_id or from_date parameter...",
  "request_id": "0d4f6758-1ce3-4dda-a0f3-0ccf5fcb50d6"
}

Wire field order is alphabetical because serde_json::Map uses a BTreeMap. The examples below match what curl actually emits.

Fields:

  • code: stable machine-readable error code.
  • error: human-readable error category.
  • message: top-level failure message (safe for client display).
  • details: deepest/root detail available.
  • request_id: per-request UUID. The same value is also returned in the X-Request-ID HTTP response header and in every server-side log line for this request. Quoting it is the easiest way to ask the operator to look up the corresponding traces.

Notes:

  • error_chain is logged server-side for diagnostics, but is not returned in API responses.
  • message is always present.
  • details is present on 4xx and 5xx errors emitted from the notification/watch/replay request path (parse, validation, processing, and SSE stream initialization). It is intentionally omitted from authentication errors (401/403/503) and from the streaming-auth helpers (forbidden, ECPDS service unavailable, internal misconfiguration), where the upstream service or authorization plugin does not provide a stable detailed message.
  • request_id is present on every error response body produced by aviso’s own handlers (notification, watch, replay, schema, admin, auth and ECPDS authorization helpers). The X-Request-ID HTTP response header carries the same UUID on every response (success, aviso error, or framework-level fallback); see Framework-Level Fallbacks for the fallback cases where a JSON body field is not available.
  • SSE stream initialization failures additionally include topic (the decoded logical topic) so the operator can scope log queries faster.
  • The admin endpoints (/api/v1/admin/*) and POST /api/v1/notification return typed responses where request_id is a struct field rather than a free-form JSON key, but the field name and value are the same.

How to report a problem

Capture either of these and pass them to the operator:

  1. The X-Request-ID HTTP response header. Visible to curl -i, browser devtools, every reverse proxy, and most log aggregators. Present on every response, success or failure.
  2. The request_id field in any error response body. Same UUID as the header.

For streaming responses (/api/v1/watch, /api/v1/replay), the same UUID also appears in the JSON data: payload of the very first event and in any error or connection-closing event the stream emits before terminating. The exact wire shape depends on the stream variant:

  • For a live-only watch, the first event has SSE event: live-notification and a JSON body with "type": "connection_established".
  • For a stream that begins with replay, the first event has SSE event: replay-control and a JSON body with "type": "replay_started".

In both cases the request_id field is in the JSON body alongside type. This means a user who only sees the open-ended SSE body (no header parsing) can still recover the id without running the request again. See Streaming Semantics for the full event-by-event payload table, including the SSE event: versus data.type distinction (relevant for clients using EventSource.addEventListener).

Error Telemetry Events

These event_name values are emitted in structured logs:

Event NameLevelTrigger
api.request.parse.failedwarnJSON parse/shape/unknown-field failure before domain validation.
api.request.validation.failedwarnDomain/request validation failure (400).
api.request.processing.failederrorServer-side processing/storage failure (500).
stream.sse.initialization.failederrorReplay/watch SSE initialization failure (500).

Every event carries request_id. The formatter additionally propagates event_type and topic from the surrounding request span when the handler has recorded them on the span before emitting the error log. Whether they appear depends on which step failed:

  • api.request.parse.failed never carries event_type or topic. The request body is rejected before either is known.
  • api.request.validation.failed sometimes carries event_type. Validation steps that run after the handler has parsed the schema (notify-side process_notification_request failures) include it. Steps that run before, namely the watch/replay request validator and the notify-side endpoint-mismatch check, do not.
  • api.request.processing.failed carries event_type. Storage-write failures additionally carry topic.
  • stream.sse.initialization.failed carries both event_type and topic.

In all cases, filter on request_id first; treat event_type and topic as auxiliary filters where present.

Error Code Reference

CodeHTTP StatusMeaning
INVALID_JSON400Request body is not valid JSON.
UNKNOWN_FIELD400Request contains fields outside API contract.
INVALID_REQUEST_SHAPE400JSON structure cannot be deserialized into request model.
INVALID_NOTIFICATION_REQUEST400Notification request failed business validation.
INVALID_WATCH_REQUEST400Watch request failed validation (replay/spatial/schema rules).
INVALID_REPLAY_REQUEST400Replay request failed validation (start cursor/spatial/schema rules).
UNAUTHORIZED401Missing or invalid credentials (no token, bad format, expired, bad signature).
FORBIDDEN403Valid credentials but user lacks the required role.
NOTIFICATION_PROCESSING_FAILED500Notification processing pipeline failed before storage.
NOTIFICATION_STORAGE_FAILED500Backend write operation failed.
SSE_STREAM_INITIALIZATION_FAILED500Replay/watch SSE stream could not be created.
INTERNAL_ERROR500Fallback internal error code (reserved).
SERVICE_UNAVAILABLE503Auth service (auth-o-tron) unreachable or returned an unexpected error.

Examples

Invalid replay request:

{
  "code": "INVALID_REPLAY_REQUEST",
  "details": "Cannot specify both from_id and from_date...",
  "error": "Invalid Replay Request",
  "message": "Cannot specify both from_id and from_date...",
  "request_id": "0d4f6758-1ce3-4dda-a0f3-0ccf5fcb50d6"
}

Auth error (missing credentials on a protected stream). Auth errors use four fields (code, error, message, request_id); details is not included:

{
  "code": "UNAUTHORIZED",
  "error": "unauthorized",
  "message": "Authentication is required for this stream",
  "request_id": "0d4f6758-1ce3-4dda-a0f3-0ccf5fcb50d6"
}

SSE initialization failure:

{
  "code": "SSE_STREAM_INITIALIZATION_FAILED",
  "details": "nats connect failed: timeout",
  "error": "SSE stream creation failed",
  "message": "Failed to create stream consumer",
  "request_id": "0d4f6758-1ce3-4dda-a0f3-0ccf5fcb50d6",
  "topic": "test_polygon.*.1200"
}

Framework-Level Fallbacks

A few 4xx responses are produced by the Actix HTTP framework itself before any aviso handler runs, so the body shape is whatever the framework defaults to (typically text/plain or empty) and not the JSON object documented above:

StatusTrigger
404 Not FoundRequest path does not match any registered aviso route.
405 Method Not AllowedPath matches an aviso route but the HTTP method does not.
400 Bad Request (rare)Request fails framework-level checks (malformed Content-Length, etc.) before reaching aviso’s body parsers.

For these cases:

  • code, error, message, details, and request_id JSON fields are not in the body.
  • The X-Request-ID HTTP response header is still set on 404 and 405 (the middleware stack runs before Actix’s default route-mismatch responses), so an operator can correlate the request with server logs by header alone. Errors raised even earlier in the HTTP stack (e.g., malformed Content-Length, TLS handshake failure) bypass aviso’s middleware entirely and produce no X-Request-ID; in those cases the request never reaches aviso, no log line is generated, and no correlation is possible.
  • The aviso code reference table above does not apply.

If you need a stable JSON shape on these paths, hit a known-good route (GET /health is the simplest); a 4xx from there indicates an actual aviso-handled error and follows the documented contract.