Skip to content

The REST door's own sendError overload does not narrow, so an unregistered error.code reaches the wire there too #9098

Description

@os-project-manager

Found while implementing #8087 (the dispatcher-door vocabulary gate). Filed unassigned; nobody is on it. Not fixed in that PR — it lands in packages/rest, outside that card's domain:cli file surface.

The gap

#8087's premise rested on an asymmetry between the two doors, stated in resolveThrownHttpError's module note and in packages/runtime/src/package-door-error-parity.test.ts:

The direct-mount REST door needs exactly this: sendError's closed ErrorCode parameter [...] so an unregistered code there is a failing test, not a wire answer.

That holds for sendError in @objectstack/types (packages/types/src/response-envelope.ts), whose code parameter is the closed ErrorCode:

exportfunctionsendError(res: EnvelopeResponse,status: number,code: ErrorCode,message: string,extra?: Pick<ApiError,'category'|'httpStatus'|'details'|'requestId'>,): void

But packages/rest has a second, same-named responder, and it is the one REST route modules reach for:

// packages/rest/src/error-response.tsexportfunctionsendError(res: any,error: any,object?: string): void{constresolved=resolveErrorResponse(error,object);logWithheldServerFault(error,resolved);res.status(resolved.status).json(resolved.body);}

error: any. No narrowing, no closed vocabulary, no type error.

The measured instance

sendFieldVisibilityFault in that same file (ADR-0106 D6 tier 3) hands it a code the ledger does not register:

sendError(res,{code: 'FIELD_VISIBILITY_UNRESOLVED',message: `Field visibility for object '${objectName}' could not be evaluated; the object schema is not being served.`,status: 503,});

FIELD_VISIBILITY_UNRESOLVED is in neither StandardErrorCode nor ERROR_CODE_LEDGER, so that 503 body cannot parse against ApiErrorSchema — and, because BaseResponseSchema embeds it, the whole response body fails to parse, not just the nested error object. (That last point is measured in #8087's suite, per code.)

Derived mechanically, not by eye:

$ node scripts/check-dispatcher-error-vocabulary.mjs --report
FIELD_VISIBILITY_UNRESOLVED objlit pending-registration packages/rest/src/error-response.ts

Why it is worth its own card

Two separable things, and only the first is #8846's business:

  1. The code needs a ledger row. It is already on spec: register the dispatcher conformance gate's reported error codes in ERROR_CODE_LEDGER (spec half of the #8087 ruling) #8846's registration list (reported by The dispatcher emits unregistered error.code values verbatim — three suites pin bodies that ApiErrorSchema would reject #8087's gate, which is door-agnostic), so registering it closes the immediate wire defect.

  2. The door has the same class of hole The dispatcher emits unregistered error.code values verbatim — three suites pin bodies that ApiErrorSchema would reject #8087 just closed for the dispatcher, and registering one code does not close it. The reason packages/rest was believed safe is a type that the module in question does not actually use. Any route reaching for the local sendError can put a fresh unregistered string on the wire tomorrow, silently — which is exactly the "an unswept producer just re-opens the hole" shape the maintainer ruled against on The dispatcher emits unregistered error.code values verbatim — three suites pin bodies that ApiErrorSchema would reject #8087 (2026-08-12, option B delivered as a gate).

Note the REST door's conformance suite did not catch this: like the dispatcher's did before #8087, it parses the cases it drives, and this branch is not among them.

Possible shapes, not a recommendation

The naming collision is worth a look on its own: two exported functions called sendError with different arities, different types and different strictness, one of which is documented elsewhere as the reason a whole door is safe.

Related: #8087 (the dispatcher half, ruled and gated), #8846 (the ledger registrations, which include this code).

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions