Skip to documentation
Documentation navigation

Documentation navigation

Documentation / guides

Errors and status codes

Map typed Harness errors to stable HTTP responses.

developer

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:

ConditionStatusCodeRetryable
Malformed UUID path segment400invalid_parameterfalse
Non-integer or out-of-range limit or skip400invalid_parameterfalse
Negative or non-numeric from_journal_seq400invalid_parameterfalse
Malformed JSON, invalid block envelope, or body over the configured cap400invalid_bodyfalse
Idempotency key longer than 255 bytes400invalid_parameterfalse
Authenticator returns an error401unauthorizedfalse
Session is absent from the live registry on a control route404session_not_foundfalse
Session is absent from the reader’s durable catalog on a status read404session_not_foundfalse
Same idempotency key with different raw body409idempotency_conflictfalse

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:

OperationSuccessFailure mapping
POST /v1/sessions create201500 internal for NewSession; 500 internal for Submit failure.
POST /v1/sessions/{sid}/restore200404 session_not_found only when the Rig returns serve.SessionNotFoundError; otherwise 500 internal.
POST /v1/sessions/{sid}/input200 with command_id500 internal for Submit failure.
POST /v1/sessions/{sid}/interrupt200 with interrupted500 internal for Interrupt failure.
Gate response202 with {}Gate-specific table below; unknown or non-gate failures are 500 internal.
GET /v1/sessions200500 internal for reader failure.
GET /v1/sessions/{sid}/status200404 session_not_found for typed not-found; 500 internal for read or visibility validation failure.
GET /v1/sessions/{sid}/journal200500 internal for replay or visibility validation failure.
GET /v1/sessions/{sid}/events200 stream500 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:

KindStatusCodeRetryable
not_found404gate_not_foundfalse
action_invalid400gate_action_invalidfalse
kind_mismatch400gate_kind_mismatchfalse
not_ready409gate_not_readyfalse
capacity503gate_capacitytrue
append_failed500internalfalse

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:

TypeCarries
serve.SessionNotFoundErrorRequested session UUID.
serve.LoopNotFoundErrorRequested loop UUID.
serve.StoreReadErrorOperation string and wrapped backend cause.
serve.NonPublicEventErrorRejected event visibility.
serve.InvalidParamErrorParameter name and fixed reason.
serve.InvalidAddrErrorAddress and wrapped parse cause.
serve.PublicBindWithoutAuthErrorRefused 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

← back to documentation