feat(api): KEEP-489 add structured error envelope helper + seed adoption#1279
Merged
Conversation
REST APIs across the codebase return bare prose strings ("Unauthorized",
"Invalid input"). Builders branch on regex matches against the human
message and break every copy update. Multiple hackathon teams
(ComputePool, spokenagents, AgentMesh, Aaether) independently reverse-
engineered defensive parsers.
This PR seeds the structured envelope pattern:
```json
{
"error": "wallet_not_configured",
"detail": "No wallet provisioned for chain 8453 in org X",
"hint": "POST /api/integrations/wallet to provision",
"docs": "https://docs.keeperhub.com/...",
"request_id": "req_..."
}
```
- New `lib/errors/api-envelope.ts` exports `apiError({status, code, detail,
hint?, docs?, requestId?, headers?})` returning a NextResponse with the
envelope. `ApiErrorCodes` exports canonical snake_case codes
(`unauthorized`, `insufficient_scope`, `not_found`, `invalid_input`,
`conflict`, `rate_limited`, `internal_error`) — per-route codes (e.g.
`wallet_not_configured`) live next to the route that raises them.
- `app/api/integrations/route.ts` adopted as the first migrated route.
All five error paths now return the structured envelope with a recovery
`hint`. KEEP-484 made this the most-tested endpoint, so it is the safest
pilot.
- 5 unit tests cover happy path, optional-field omission, header
preservation, and the canonical-code export.
Scope is explicitly **Phase 1**: ship the helper and one migrated route so
the pattern is callable and tested. The remaining ~100 routes can migrate
opportunistically by other PRs that touch them; this is by design — a
single mega-PR migrating every route would be impossible to review and
high-risk for regressions.
Pairs with KEEP-474 (MCP session bootstrap errors now use JSON-RPC's own
envelope; both share the same `code` / `hint` philosophy).
…test, docs - Resolve request_id from x-request-id header (validated, length-capped) with crypto.randomUUID fallback; echo back as x-request-id response header so clients can correlate. - Sanitize detail in production: strip file-path and stack-trace fragments, length-cap to 1024 chars. Dev/test pass through unchanged. - Add integration test for /api/integrations GET unauthenticated asserting the envelope shape (request_id present, no legacy message field) and request_id round-trip from the x-request-id header. - Update docs/api/index.md to document the actual error envelope shape exposed at /api/* boundaries.
🧹 PR Environment Cleaned UpThe PR environment has been successfully deleted. Deleted Resources:
All resources have been cleaned up and will no longer incur costs. |
ℹ️ No PR Environment to Clean UpNo PR environment was found for this PR. This is expected if:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
lib/errors/api-envelope.tsexportsapiError({status, code, detail, hint?, docs?, requestId?})returning a NextResponse with the structured envelope{error, detail, hint, docs, request_id}.app/api/integrations/route.tsadopted as the first migrated route — all 5 error paths now return the envelope with a recovery hint.Linear
KEEP-489 — Replace generic error strings with structured {code, detail, hint} envelope across all APIs
Repro (now passes)
Before:
curl -s /api/integrations(unauthenticated) ->{\"error\": \"Unauthorized\"}— no code beyond what's already in the string, no recovery hint, no way for client to differentiate "no header" vs "bad token" vs "expired" without parsing the human message.After: same call ->
{\"error\": \"unauthorized\", \"detail\": \"Missing Authorization header\", \"hint\": \"Provide a valid \\Authorization: Bearer \header. API keys start with \\kh_\; OAuth tokens are issued via /oauth/authorize.\"}. Clients branch onerror == \"unauthorized\"(stable code) and surfacehintto the developer.Test plan
pnpm test api-error-envelope— 5/5 pass:{error, detail}with requested statuspnpm checkandpnpm type-checkclean for changed files/api/integrationsmigrated and exercise the new shapeScope: Phase 1
This PR seeds the helper and migrates one representative route. The remaining ~100 routes will migrate opportunistically (each PR that touches a route is expected to migrate its error paths). A single mega-PR is impossible to review and high-risk for regressions.
Pairs with KEEP-474 (MCP session bootstrap errors got JSON-RPC's own envelope; both share
code+hintphilosophy).Out of scope
request_idpropagation — accepted as an optional field but no infrastructure yet generates them. Tracked as follow-up.docsURL conventions — the helper accepts the field; per-error doc anchors are a separate docs project.WALLET_NOT_CONFIGUREDadvertise-vs-call discrepancy is atools/listschema issue, not an error-shape issue.