Skip to content

[finding] registerSecurityEndpoints answers THREE different error-envelope shapes on the same three routes (ADR-0112) #7981

Description

@hotlong

observation-class finding, recorded while fixing #7678 (PR #7979). Not fixed there — that card was scoped to the ?status validation, and this is a separate, wider shape. Filed unassigned.

Fact

The three /api/v1/security/suggested-bindings routes in packages/rest/src/rest-server.tsregisterSecurityEndpoints emit three mutually incompatible error envelopes, depending only on which arm refuses:

armshapewhere
repeated query param (#6877) — and, since #7678, unknown ?status{ error: { code, message } }refuseRepeatedQueryParams / the new status guard
service not registered{ code, message }no error wrapper at allrespond501
thrown service error (403 / 404 / 409 / 500){ code, error: "<string>" }error is a bare stringhandleError

So a client cannot read one field to learn why its call failed on these routes: error is an object in one arm, absent in another, and a human-readable string in the third — and the semantic code sits at error.code in the first and at top-level code in the other two.

The third shape is specifically the dialect #7035 retired (PR #7293 converged this file's /meta 501 refusals off it). handleError is the arm that carries the typed service errors the route's own docblock advertises — permission 403, not-found 404, state-conflict 409 — i.e. the arm consumers are most likely to branch on.

Why it is worth recording separately

Under ADR-0112 D5 the destination is fixed: error.code carries the semantic code, the HTTP status lives on the transport. All three arms here predate that convergence, and none of them is wrong individually — they are wrong as a set, which is exactly the class no per-arm review catches.

It is also a behaviour divergence against the runtime dispatcher twin (packages/runtime/src/domains/security.ts), which routes every one of these outcomes through deps.error / deps.errorFromThrownapiErrorResponse, i.e. one shape: { success: false, error: { code, message, httpStatus } }. That makes it a concrete input to the route-ledger↔live-mount parity gate #7526, and specifically to the argument #7678 makes — that the gate should cover behaviour divergence, not just mount presence. A gate comparing only mount presence is green on all three of these.

Not claimed here

Suggested handling

Triage-level, not decided here: converge the three arms in registerSecurityEndpoints onto the nested { error: { code, message } } shape the same route already uses for its two validation refusals — or fold it into whatever batch is carrying the #7035 convergence through rest-server.ts, since this is the same edit repeated per route family.


Generated by Claude Code

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions