Documentation / guides
Errors and status codes
Map typed Harness errors to stable HTTP responses.
The serve package returns one nested JSON error shape for every handler-level failure. Status selection is stable and generic; internal causes are logged, not serialized.
Error envelope
The wire shape is:
{
"error": {
"code": "session_not_found",
"message": "session not found",
"retryable": false
}
}
Content-Type is application/json. code is the machine branch, message
is a client-safe summary, and retryable tells a caller whether retrying the
same request can make sense. The handler passes a cause to its logger through
writeErrorCause; the body never includes cause.Error().
Request errors
These failures happen before a session or reader operation is invoked:
| Condition | Status | Code | Retryable |
|---|---|---|---|
| Malformed UUID path segment | 400 | invalid_parameter | false |
Non-integer or out-of-range limit or skip | 400 | invalid_parameter | false |
Negative or non-numeric from_journal_seq | 400 | invalid_parameter | false |
| Malformed JSON, invalid block envelope, or body over the configured cap | 400 | invalid_body | false |
| Idempotency key longer than 255 bytes | 400 | invalid_parameter | false |
| Authenticator returns an error | 401 | unauthorized | false |
| Session is absent from the live registry on a control route | 404 | session_not_found | false |
| Session is absent from the reader’s durable catalog on a status read | 404 | session_not_found | false |
| Same idempotency key with different raw body | 409 | idempotency_conflict | false |
The parser uses typed serve.InvalidParamError{Param, Reason} values. It does
not echo the supplied value. limit defaults to 100 and accepts 1 through
1000; skip defaults to 0 and must be nonnegative; from_journal_seq defaults
to 0 and parses as an unsigned 64-bit cursor.
Operation errors
After boundary validation, operation failures use these responses:
| Operation | Success | Failure mapping |
|---|---|---|
POST /v1/sessions create | 201 | 500 internal for NewSession; 500 internal for Submit failure. |
POST /v1/sessions/{sid}/restore | 200 | 404 session_not_found only when the Rig returns serve.SessionNotFoundError; otherwise 500 internal. |
POST /v1/sessions/{sid}/input | 200 with command_id | 500 internal for Submit failure. |
POST /v1/sessions/{sid}/interrupt | 200 with interrupted | 500 internal for Interrupt failure. |
| Gate response | 202 with {} | Gate-specific table below; unknown or non-gate failures are 500 internal. |
GET /v1/sessions | 200 | 500 internal for reader failure. |
GET /v1/sessions/{sid}/status | 200 | 404 session_not_found for typed not-found; 500 internal for read or visibility validation failure. |
GET /v1/sessions/{sid}/journal | 200 | 500 internal for replay or visibility validation failure. |
GET /v1/sessions/{sid}/events | 200 stream | 500 internal when subscription cannot be created; 404 session_not_found for a registry miss. |
The live control routes consult the process-local registry. The read routes use
the injected Reader and do not require a live session. A 404 on a control
route therefore means only that the requested ID is not live in this process.
Gate and stream mappings
Gate failures are selected by the stable GateErrorKind() value:
| Kind | Status | Code | Retryable |
|---|---|---|---|
not_found | 404 | gate_not_found | false |
action_invalid | 400 | gate_action_invalid | false |
kind_mismatch | 400 | gate_kind_mismatch | false |
not_ready | 409 | gate_not_ready | false |
capacity | 503 | gate_capacity | true |
append_failed | 500 | internal | false |
An SSE connection sends headers and status 200 before waiting for events.
After that point an error cannot be changed into a JSON response. The stream
closes on subscription close, request cancellation, write or flush failure, or
an encoding failure. Enduring frames carry their journal sequence as the SSE
id, so a reconnecting client can use the journal read endpoint to recover a
durable gap.
%%{init: {"theme":"dark"}}%%
flowchart TD
A[Request] --> B{Auth callback}
B -- reject --> C[401 unauthorized]
B -- allow --> D{Path, query, body validation}
D -- reject --> E[400 invalid_parameter or invalid_body]
D -- allow --> F{Operation}
F -- live registry miss --> G[404 session_not_found]
F -- gate capacity --> H[503 gate_capacity retryable]
F -- gate semantic error --> I[400 or 409 gate code]
F -- backend or runtime error --> J[500 internal]
F -- success --> K[JSON or SSE response]
Typed errors and cause boundaries
The handler’s typed errors are intended for trusted logs and tests:
| Type | Carries |
|---|---|
serve.SessionNotFoundError | Requested session UUID. |
serve.LoopNotFoundError | Requested loop UUID. |
serve.StoreReadError | Operation string and wrapped backend cause. |
serve.NonPublicEventError | Rejected event visibility. |
serve.InvalidParamError | Parameter name and fixed reason. |
serve.InvalidAddrError | Address and wrapped parse cause. |
serve.PublicBindWithoutAuthError | Refused address. |
StoreReadError and InvalidAddrError implement Unwrap; use
errors.As or errors.Is in trusted code rather than parsing error text.
var readErr serve.StoreReadError
if errors.As(err, &readErr) {
log.Printf("read operation %q failed: %v", readErr.Op, readErr)
}
No typed cause is converted to a public message by default. This is the boundary that keeps storage paths, provider responses, credentials, and PII out of HTTP bodies.
Source and runnable proof
The nested envelope and serve-level typed errors are in
errors.go.
Path/query parsing is in
parse.go,
and route mappings are implemented in
handlers_lifecycle.go,
handlers_control.go,
handlers_read.go,
handlers_events.go,
and handlers_gate.go.
The response fixtures and route cases are in
fixtures_test.go,
handlers_read_test.go,
handlers_control_test.go,
handlers_gate_test.go,
and handlers_events_test.go.
Run:
go test ./pkg/serve