Summary
Add a small, production-focused GraphQL error contract so clients can handle Atom failures without parsing human-readable messages.
Atom must keep normal GraphQL-over-HTTP behavior: an execution or resolver error may return HTTP 200, and partial responses may contain both data and errors. This issue adds stable metadata; it does not convert GraphQL execution errors into transport failures.
Current state
- The base GraphQL handler returns the async-graphql response envelope and preserves partial-data semantics.
- AppError already distinguishes not found, bad request, unauthenticated, forbidden, conflict, payload too large, rate limited, service unavailable, database, and internal errors.
- The GraphQL error adapter sanitizes database/internal messages, but exposes no stable machine-readable code.
- Transport authentication failures also return message-only GraphQL errors.
- There is no general HTTP request/correlation ID for GraphQL responses.
- The Atom Next UI rejects non-empty GraphQL errors, but forbidden detection currently depends on message text.
- OpenAPI documents the errors array, but does not define the extensions contract.
Relevant files:
- src/graphql/mod.rs
- src/graphql/auth.rs
- src/error.rs
- src/routes.rs
- src/audit.rs
- app/lib/graphql/client.ts
- app/lib/graphql/server.ts
- apidocs/openapi.yaml
Short PRD
Problem
HTTP success only means that the GraphQL server processed the request. A non-empty errors array means one or more GraphQL fields failed. Message text is not a stable API and cannot safely carry internal details.
Goal
Every Atom-generated GraphQL error has stable machine-readable metadata and a correlation ID. Clients can distinguish failures and decide whether to retry without parsing message.
V1 error shape
{
"message": "forbidden",
"path": ["updateResource"],
"extensions": {
"code": "FORBIDDEN",
"retryable": false,
"requestId": "2cc55e48-d44f-49ce-99e2-b08f06f619a6"
}
}
RATE_LIMITED errors may additionally include retryAfterSeconds. The same request ID should be returned through the X-Request-ID response header.
Stable V1 codes
| Code |
Meaning |
Retryable |
| BAD_REQUEST |
Invalid GraphQL syntax, validation, input, or reference |
No |
| UNAUTHENTICATED |
Authentication is missing, expired, revoked, or invalid |
No |
| FORBIDDEN |
The authenticated caller is not allowed |
No |
| NOT_FOUND |
The requested object does not exist or is not visible |
No |
| CONFLICT |
The request conflicts with current state |
No |
| PAYLOAD_TOO_LARGE |
The accepted payload limit was exceeded |
No |
| RATE_LIMITED |
A request limit was exceeded |
Yes |
| SERVICE_UNAVAILABLE |
A required dependency is temporarily unavailable |
Yes |
| INTERNAL |
Atom could not safely complete the operation |
No |
Required behavior
- Preserve HTTP 200 for normal GraphQL execution/resolver errors.
- Preserve partial data, error paths, and source locations.
- Inspect both HTTP status and the response errors array in clients.
- Never expose SQL, database messages, secrets, stack traces, or policy internals.
- Use one request ID in the response header and every GraphQL error in that response.
- Accept an incoming X-Request-ID only if it is a valid bounded identifier; otherwise generate one server-side.
- Keep custom REST-shaped endpoint HTTP behavior unchanged in V1.
Implementation scope
1. Contract document
Add a short product document defining the response shape, code table, retry behavior, security rules, and examples. Link it from the main product requirements and GraphQL documentation.
2. Core server contract
- Add one exhaustive mapping from AppError to public code, safe message, retryable, and optional retryAfterSeconds.
- Reuse that mapping in the GraphQL error adapter instead of duplicating message rules.
- Add stable extensions to resolver, authentication, parse, validation, callout-deny, and other Atom-generated GraphQL errors.
- Add request-ID middleware and propagate the ID into GraphQL request context and the X-Request-ID response header.
- Add requestId to every GraphQL error without replacing data, path, locations, or existing safe extensions.
- Log internal details server-side while returning only INTERNAL or safe database-derived conflict/validation messages.
3. Reference client behavior
Update the existing Atom TypeScript helper:
- Model extensions.code, retryable, requestId, and retryAfterSeconds.
- Detect forbidden using code rather than message.
- Keep an all-or-nothing helper that rejects every non-empty errors array.
- Provide a raw result path for callers that deliberately consume partial query data and errors together.
- Apply the same decoding behavior to browser and server GraphQL clients.
A standalone published SDK is not required for this issue.
4. API documentation
- Define the extensions schema in apidocs/openapi.yaml.
- Document HTTP status versus GraphQL operation success.
- Document partial data and retry behavior.
- Document the X-Request-ID response header.
- Distinguish the base GraphQL endpoint from custom REST-shaped endpoints.
Essential tests
Rust contract tests
Cover at minimum:
- malformed GraphQL syntax or validation failure -> BAD_REQUEST;
- missing/invalid authentication -> UNAUTHENTICATED;
- authorization denial -> FORBIDDEN;
- missing object -> NOT_FOUND;
- unique-state conflict -> CONFLICT;
- rate limit -> RATE_LIMITED with retryable true and retryAfterSeconds when known;
- unavailable dependency -> SERVICE_UNAVAILABLE with retryable true;
- database/internal failure -> INTERNAL with no sensitive details;
- one multi-field response containing both partial data and errors;
- request ID matches between X-Request-ID and every returned error;
- existing custom endpoint HTTP behavior remains unchanged.
Each test should assert HTTP status, data, errors, code, retryable, requestId, path where applicable, and absence of sensitive details.
TypeScript client tests
Cover at minimum:
- success returns data;
- HTTP 200 with non-empty errors is rejected by the strict helper;
- FORBIDDEN is detected from extensions.code, not message;
- partial data is preserved by the raw result helper;
- requestId is available on the thrown client error;
- transport failures without a GraphQL envelope are normalized safely.
Non-goals
- Publishing a separate SDK package.
- Redesigning custom endpoint HTTP status mapping.
- Adding request IDs to every persisted audit row.
- Defining a unique code for every domain validation message.
- Changing GraphQL partial-data semantics.
Production acceptance criteria
- No supported client parses human-readable messages to classify Atom errors.
- Every Atom-generated GraphQL error contains code, retryable, and requestId.
- Internal/database details never appear in public responses.
- HTTP 200 plus a non-empty errors array cannot be mistaken for mutation success.
- Partial query data remains representable and test-covered.
- OpenAPI and product documentation match runtime behavior.
- cargo test, cargo clippy -- -D warnings, cargo fmt --check, the Atom UI unit tests, and UI lint all pass.
Rollout
This is additive to the GraphQL response extensions and should not change successful response data or the frozen GraphQL SDL. Release server and bundled UI changes together, verify the contract in staging, then promote to production.
Summary
Add a small, production-focused GraphQL error contract so clients can handle Atom failures without parsing human-readable messages.
Atom must keep normal GraphQL-over-HTTP behavior: an execution or resolver error may return HTTP 200, and partial responses may contain both data and errors. This issue adds stable metadata; it does not convert GraphQL execution errors into transport failures.
Current state
Relevant files:
Short PRD
Problem
HTTP success only means that the GraphQL server processed the request. A non-empty errors array means one or more GraphQL fields failed. Message text is not a stable API and cannot safely carry internal details.
Goal
Every Atom-generated GraphQL error has stable machine-readable metadata and a correlation ID. Clients can distinguish failures and decide whether to retry without parsing message.
V1 error shape
RATE_LIMITED errors may additionally include retryAfterSeconds. The same request ID should be returned through the X-Request-ID response header.
Stable V1 codes
Required behavior
Implementation scope
1. Contract document
Add a short product document defining the response shape, code table, retry behavior, security rules, and examples. Link it from the main product requirements and GraphQL documentation.
2. Core server contract
3. Reference client behavior
Update the existing Atom TypeScript helper:
A standalone published SDK is not required for this issue.
4. API documentation
Essential tests
Rust contract tests
Cover at minimum:
Each test should assert HTTP status, data, errors, code, retryable, requestId, path where applicable, and absence of sensitive details.
TypeScript client tests
Cover at minimum:
Non-goals
Production acceptance criteria
Rollout
This is additive to the GraphQL response extensions and should not change successful response data or the frozen GraphQL SDL. Release server and bundled UI changes together, verify the contract in staging, then promote to production.