Skip to content

feat(api): KEEP-489 add structured error envelope helper + seed adoption#1279

Merged
eskp merged 2 commits into
stagingfrom
simon/keep-489-structured-error-envelope
May 18, 2026
Merged

feat(api): KEEP-489 add structured error envelope helper + seed adoption#1279
eskp merged 2 commits into
stagingfrom
simon/keep-489-structured-error-envelope

Conversation

@eskp
Copy link
Copy Markdown

@eskp eskp commented May 18, 2026

Summary

  • REST APIs returned bare prose strings ("Unauthorized", "Invalid input"). Multiple hackathon teams reverse-engineered defensive parsers because the messages had no machine-readable code or recovery hint.
  • New lib/errors/api-envelope.ts exports apiError({status, code, detail, hint?, docs?, requestId?}) returning a NextResponse with the structured envelope {error, detail, hint, docs, request_id}.
  • app/api/integrations/route.ts adopted as the first migrated route — all 5 error paths now return the envelope with a recovery hint.
  • 5 unit tests cover envelope shape, optional-field omission, header preservation, canonical-code export.

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 on error == \"unauthorized\" (stable code) and surface hint to the developer.

Test plan

  • pnpm test api-error-envelope — 5/5 pass:
    • Returns {error, detail} with requested status
    • Includes optional hint/docs/request_id when provided
    • Omits optional fields when not provided
    • Preserves caller-supplied headers (e.g. Retry-After on 429)
    • Canonical codes export the documented strings
  • pnpm check and pnpm type-check clean for changed files
  • All 5 error paths in /api/integrations migrated and exercise the new shape

Scope: 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 + hint philosophy).

Out of scope

  • Migrating every existing route (would be a 100+ file diff — unreviewable).
  • request_id propagation — accepted as an optional field but no infrastructure yet generates them. Tracked as follow-up.
  • docs URL conventions — the helper accepts the field; per-error doc anchors are a separate docs project.
  • The Aaether WALLET_NOT_CONFIGURED advertise-vs-call discrepancy is a tools/list schema issue, not an error-shape issue.

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.
@eskp eskp merged commit f1b9647 into staging May 18, 2026
42 checks passed
@eskp eskp deleted the simon/keep-489-structured-error-envelope branch May 18, 2026 21:33
@github-actions
Copy link
Copy Markdown

🧹 PR Environment Cleaned Up

The PR environment has been successfully deleted.

Deleted Resources:

  • Namespace: pr-1279
  • All Helm releases (Keeperhub, Scheduler, Event services)
  • PostgreSQL Database (including data)
  • LocalStack, Redis
  • All associated secrets and configs

All resources have been cleaned up and will no longer incur costs.

@github-actions
Copy link
Copy Markdown

ℹ️ No PR Environment to Clean Up

No PR environment was found for this PR. This is expected if:

  • The PR never had the deploy-pr-environment label
  • The environment was already cleaned up
  • The deployment never completed successfully

@eskp eskp mentioned this pull request May 18, 2026
12 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant