Skip to content

settings error bodies carry four keys ApiErrorSchema does not declare, conformant only because z.object strips them #4224

Description

@os-zhuang

Surfaced while consolidating the seven envelope builders onto one shared pair (#3973). Filed rather than folded in, because fixing it is a wire change and #3973 was explicitly a consolidation that changes no bytes.

The finding

settings-routes.ts passes ad-hoc keys as SIBLINGS of code and message inside error:

sendError(res, 403, 'SETTINGS_FORBIDDEN', err.message, { namespace: err.namespace });
sendError(res, 409, 'SETTINGS_LOCKED',    err.message, { namespace, key, reason });
sendError(res, 400, 'UNKNOWN_KEY',        err.message, { namespace, key });
sendError(res, 400, 'SETTINGS_VALIDATION', err.message, { namespace, fields });

ApiErrorSchema (packages/spec/src/api/contract.zod.ts) declares code, message, category?, httpStatus?, details?, requestId?. namespace / key / reason / fields are none of those. Because the schema is a plain z.object, unknown keys are stripped rather than rejected — so these bodies are conformant by stripping, not by declaration, and envelopeViolations (which only inspects the body's top level) does not see them either.

The same module already uses the declared slot correctly one branch over:

sendError(res, 400, 'SETTINGS_ACTION_FAILED', result.message ?? '…', { details: result });

so this is an inconsistency inside one file, not a missing capability.

Why it is not urgent

Nothing in the repo reads error.namespace / error.key / error.reason / error.fieldsgrep across packages/, examples/ finds no consumer. They are write-only today.

Why it is worth closing anyway

It is the reason the shared sendError types its last parameter Record<string, unknown> rather than the declared optional fields of ApiError:

// packages/types/src/response-envelope.ts
extra?: Record<string, unknown>,

That is the one loose edge on an otherwise closed builder — the same builder that now types code as the closed ADR-0112 ErrorCode union. Move settings' four call sites under details and the parameter can be tightened to the declared fields, which closes the envelope's error half the way #3841 closed its vocabulary.

Decision needed

Whether error.details is the right home (it is the declared slot, and the module already uses it), or whether these belong in the fields[] field-level vocabulary that ADR-0112 D6 / #3977 carve out — SETTINGS_VALIDATION in particular passes fields, which reads like the latter.

Either way it is a change to what /api/settings/* puts on the wire for four error branches, so it wants a changeset with the FROM → TO mapping.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions