Files
hapi/docs/api/client-contract/errors.md
T

8.6 KiB
Raw Blame History

Errors

Error semantics for all /api/* endpoints. Grounded in hub/src/web/routes/guards.ts and the individual route files under hub/src/web/routes/; reference consumer web/src/api/client.ts (ApiError, parseErrorCode).

Body shape

Error responses are JSON:

{ "error": "Session is inactive", "code": "session_inactive" }
  • error — human-readable message. Never match on it: consumers i18n it and the hub may reword it (this rule is stated in guards.ts itself).
  • code — optional stable machine-readable discriminator. Clients branch on (status, code).
  • issues — present on some 400s: Zod validation details (parsed.error.issues or .flatten() output). Useful for logging, not for branching.
  • A few endpoints add context fields (e.g. 413 export adds count/limit; 422 reopen adds missing[]).

When no code is present, branch on status alone and treat the failure generically. The web reference falls back to using error as a pseudo-code when code is absent (parseErrorCode) — acceptable for logging, not for logic.

Status × code table

Status code Where (source) Meaning / client action
400 — All routes with Zod bodies ({error: 'Invalid body'}, some with issues) Client bug — fix the request; do not retry
400 — Flavor gates (sessions.ts: wrong-flavor model/mode endpoints) Hide the control for this flavor
400 scratchlist_attachment_invalid, scratchlist_entry_empty, attachment-limit codes from validateScratchlistAttachmentsForWrite sessions.ts scratchlist routes Surface validation message
401 — Middleware (middleware/auth.ts) and POST /api/auth (routes/auth.ts) See Auth → 401 bodies; middleware 401 → silent re-auth once, /api/auth 401 → re-pair
403 — guards.ts (Session access denied, Machine access denied); owner-only routes (usage.ts, storage.ts, hubSettings.ts, voice.ts) Namespace mismatch / not hub owner — hide the surface, don't retry
403 access_denied RPC-flow results mapped in sessions.ts (resume/reopen/cursor-chat-store) Same as above
404 — guards.ts (Session not found, Machine not found); permissions.ts (Request not found); scratchlist entry/attachment; events.ts visibility (Subscription not found) Stale reference — refresh the parent list
404 session_not_found, machine_not_found Coded variants from resume/reopen/restart-runner result mapping Same
409 session_inactive guards.ts requireSession(requireActive) — send/steer/abort/approve/deny/config on an inactive session Offer Reopen (the web router does exactly this on this code)
409 scratchlist_at_cap sessions.ts scratchlist create (200-entry cap) Show cap notice; do not retry
409 scratchlist_attachment_in_use sessions.ts attachment delete while still referenced Detach from entry first
409 resume_unavailable sessions.ts resume/reopen result mapping Session can't be resumed (e.g. unsupported state)
409 metadata_conflict sessions.ts reopen result mapping Refetch session, retry once at most
409 runner_upgrade_required machines.ts Agent availability Upgrade and restart the runner; disable session creation
409 control_mode_not_applicable sessions.ts switch Concurrent clients do not use ownership switching; hide takeover controls
409 — (version conflict) sessions.ts PATCH rename/summary, machines.ts PATCH rename — message mentions version/concurrently; no code Concurrent edit — refetch and reapply
409 — sessions.ts delete-while-active, archive of plain inactive row, fork/rewind refusals, remote-only config on terminal-controlled sessions (controlledByUser) Surface message; refresh session state
413 — sessions.ts upload (> 50 MB decoded), export too large ({error, count, limit}); voice.ts transcription (Audio file too large, 25 MB audio / ~26 MB body) Reduce payload
413 scratchlist_attachment_too_large sessions.ts scratchlist upload Reduce attachment
422 — sessions.ts reopen with incomplete metadata ({error, missing[]}); title-suggestion pass-through (TitleSuggestionError, statuses 422/429/502/503) Not reopenable / feature unavailable
429 — Title suggestion (provider rate limit) Back off
500 — Catch-all in most routes ({error: message}) Log; generic failure UI
502 — Title suggestion upstream failure; restart-runner unknown error Retry later
503 — guards.ts requireSyncEngine → {error: 'Not connected'} — hub subsystems not up (startup/shutdown window); also Telegram-disabled on /api/auth initData path Retry with backoff
503 no_machine_online resume/reopen/spawn-flow result mapping (sessions.ts) The machine that owns the session is offline — tell the user to start the runner
503 machine_offline machines.ts restart-runner Same
503 rpc_target_missing machines.ts pi/codex model catalogs (RPC_TARGET_MISSING_ERROR_CODE in shared/src/rpcMethods.ts — RPC handler unregistered or socket disconnected) CLI-side target gone — treat as offline

RPC-wrapped endpoints

Many endpoints do not answer from hub state — the hub relays the request over Socket.IO to the session's CLI process (or the machine's runner) and forwards the result: git/file/directory/search, generated images, uploads, model catalogs, Agent availability, slash-commands, skills, spawn, list-directory, paths/exists. (The mode/model/effort config endpoints are RPC-backed too, but map apply-failures to 409 with a message.) Their failure modes differ from plain endpoints:

  1. CLI reachable, command failed → HTTP 200 with {success: false, error} (e.g. runRpc in hub/src/web/routes/git.ts catches RPC errors, including the 30 s RPC timeout, and returns them as a JSON envelope). Clients must check the success field on every RPC-shaped response; HTTP 200 alone means nothing.
  2. CLI offline / handler missing → depends on the route: the model-catalog routes in machines.ts map RpcTargetMissingError to 503 rpc_target_missing; git.ts-style routes fold it into the 200 {success: false} envelope; resume/reopen surface 503 no_machine_online.
  3. Hub subsystems not up → 503 Not connected from requireSyncEngine (brief startup/shutdown window).

Explicit rpc_target_missing / no_machine_online failures mean the execution host or handler is unavailable. Do not infer that from success: false alone: a reachable CLI can report a command, path, permission, or validation failure. Preserve that error for display; HTTP success is not operation success.

One route qualifies that rule. GET /api/machines/:id/agy-models keeps serving the last catalog the machine got out of agy models while the CLI re-checks in the background, so it can answer success: true and carry an error: the list is usable, and error says why it may be stale (typically the machine's agy sign-in has lapsed). Render it beside the catalog rather than instead of it, and offer ?refresh=true as the way to ask again — a plain repeat is answered from the same cache.

When that background re-check lands a different listing, the machine says so over the existing event stream rather than making clients ask: machine-agy-models-updated (see SSE) carries the machineId and nothing else. Refetch that machine's route on it — the answer comes from the machine's cache, so it costs no agy run and produces no further event. It is emitted whenever the re-check changes what this route would answer — a different listing, or a sign-in warning that appeared or cleared — and not when it changes neither, so a failed re-check that raises a warning does announce. The machine's very first listing is never announced: whoever triggered it is already awaiting it. Requests during that window are answered from the machine's cache and do not launch agy.

Retry guidance

Class Retry?
400 / 403 / 404 / 409 / 413 / 422 No (fix input, refresh state, or hide surface)
401 (middleware) Once, after silent re-auth (Auth)
429 / 502 / 503 Reads: retry with backoff. Mutations: follow the endpoint's recovery contract; do not replay a write whose outcome is unknown
200 {success: false} Inspect the error and endpoint contract; manual retry only when safe. This envelope can also represent a missing RPC target or timeout