[AVAILABLE].
packages/api/src/middleware/error-handler.ts, verified against a live server in this session.The envelope
Every non-2xx response (exceptPOST /policies/validate, see below) is:
code is present only when the failure reached the shared error handler as a typed
RuntimeError subclass: VerificationFailedError (VERIFICATION_FAILED),
ReceiptGenerationError (RECEIPT_GENERATION_FAILED), or an uncategorized RuntimeError
(RUNTIME_ERROR). It is absent from every inline route-level check
(businessTransactionId format/required checks), from
BusinessTransactionValidationError, PolicyValidationError, SignalValidationError,
PolicyNotFoundError, DuplicateBusinessTransactionError, and from the generic 500
fallback that catches everything else.
error is always a plain string, never a nested object, this is deliberate: a caller can
always safely display it or log it without walking a schema first.
POST /policies/validate does not use this envelope at all. Every status code it can
return (200, 400, 404) is {"valid": boolean, "errors": string[]}, its own shape,
independent of what the caller sent. The one exception is 401, which is generated by the
caller-auth middleware before this handler ever runs, so a rejected caller sees the shared
envelope ({"error": "authentication required"}), not {valid, errors}.
Why code is inconsistent by design, not by accident
The envelope was built to describe whatever the code actually raises, not a target
contract layered on afterward. Two verified gaps below show what that means in practice:
places where an error path that looks like it should carry a stable code in fact does not,
because the exception that reaches the handler isn’t the typed class the handler’s
instanceof checks expect.
Two verified gaps
1. A structurally incomplete body crashes uncoded
APOST /execute or POST /transactions body with a valid-UUID businessTransactionId but
a missing required nested object (for example metadata) does not reach the 400
BusinessTransactionValidationError path the schema implies exists for it. It throws an
unhandled TypeError deep inside BusinessTransactionValidator.validate
(packages/runtime/src/validators/BusinessTransactionValidator.ts:14), which is not an
instance of BusinessTransactionValidationError, so the handler’s instanceof check never
matches. The caller sees only:
code, and no indication of which field was missing. The real cause is
visible only in the server’s own log.
2. A regression: “no Connector registered” lost its code, this pass
Re-verified live against the current server, not carried over from an earlier capture: the “no Connector registered for this action” case used to reach the client as a codedRuntimeError ({"error":"No connector registered for action: <action>.","code": "RUNTIME_ERROR"}). It no longer does. ConnectorSdkRegistry.resolveCapability
(packages/connector-sdk/src/ConnectorRegistry.ts) now throws a raw, uncaught Error: No connector registered for capability '<capability>'. that also fails the handler’s
instanceof RuntimeError check, collapsing into the exact same shape as gap #1:
packages/connector-sdk/packages/execution-control, most likely a side effect of the
recent capability-routing refactor, not a documentation change made in this pass. See the
Error catalog for the full, current, real behavior of every
route.