--- id: http/gateway/error-mapping name: Implement CallError-to-HTTP-status error mapping (ADR-023) status: pending depends_on: [http/crate-init] scope: narrow risk: low impact: component level: implementation --- ## Description Implement the `CallError` code → HTTP status code mapping in `src/gateway/error.rs`. This is the error-mapping table the HTTP server's gateway endpoints use to translate call-protocol `CallError` codes into HTTP response status codes (ADR-023). The mapping is a two-way-door default (the exact status for ambiguous codes can be refined additively); the one-way constraint is that protocol-level and operation-level codes are distinct (ADR-023) and `from_openapi`-imported codes are prefixed `HTTP_` to avoid collision with protocol codes. ### The mapping table (http-server.md §"Error Mapping") | Call `code` | HTTP status | Notes | |-------------|-------------|-------| | `NOT_FOUND` (operation not registered, or Internal op) | `404` | | | `FORBIDDEN` (insufficient scopes, or unauthenticated) | `401` (no token) / `403` (token present) | | | `INVALID_INPUT` (schema mismatch) | `422` | | | `TIMEOUT` | `504` | `retryable: true` | | `INTERNAL` | `500` | | | Operation-level domain code with `http_status` (ADR-023) | the declared `http_status` | `from_openapi`-imported ops carry the original status | | Operation-level domain code without `http_status` | `500` | | ### The `HTTP_` prefix rule (ADR-023 §5) `from_openapi` maps OpenAPI non-2xx response status codes to `ErrorDefinition`s with codes prefixed `HTTP_` + the status number: ```rust // OpenAPI: 404: { schema: NotFoundError } // → ErrorDefinition { code: "HTTP_404", http_status: Some(404), schema: NotFoundError } ``` The normative rule (review #002 W20): `from_openapi` must not produce error codes that collide with the five protocol-level codes (`NOT_FOUND`, `FORBIDDEN`, `INVALID_INPUT`, `INTERNAL`, `TIMEOUT`). The `HTTP_` prefix enforces this. ### `retryable` → `Retry-After` hint The `retryable` field from `CallError` maps to an HTTP `Retry-After` hint for `503`/`429`-class errors (operation-level codes with `http_status` in that range). The hint is optional; if the operation-level error does not carry a retry-after value, no header is added. ### API ```rust /// Map a CallError to an HTTP status code (ADR-023). pub fn call_error_to_http_status(error: &CallError) -> u16; /// Map a CallError to an HTTP response, including the Retry-After hint /// when applicable. The body is the serialized CallError (or its /// `details` field). pub fn call_error_to_http_response(error: &CallError) -> axum::response::Response; ``` The `FORBIDDEN` case needs the caller's identity state to distinguish `401` (no token) from `403` (token present but insufficient scopes). The mapping function takes an `Option` (or a flag) so the gateway endpoint can pass the resolved identity through: ```rust /// Map a CallError to an HTTP status code, considering whether the caller /// was authenticated (FORBIDDEN → 401 if no identity, 403 if identity /// present but insufficient scopes). pub fn call_error_to_http_status_with_identity( error: &CallError, identity: Option<&Identity>, ) -> u16; ``` ### What this task does NOT do - **No `to_openapi` error projection.** `to_openapi` projects `error_schemas` to the gateway endpoint's response definitions (the OpenAPI doc's `responses` block). That is the `to-openapi` task, not this one. This task is the runtime HTTP response mapping. - **No `from_openapi` error import.** `from_openapi` builds `ErrorDefinition`s from OpenAPI non-2xx responses with the `HTTP_` prefix. That is the `from-openapi` task. This task consumes the resulting `CallError` codes at runtime. ## Acceptance Criteria - [ ] `call_error_to_http_status(error: &CallError) -> u16` implemented - [ ] `NOT_FOUND` → 404 - [ ] `FORBIDDEN` → 401 (no identity) / 403 (identity present) - [ ] `INVALID_INPUT` → 422 - [ ] `TIMEOUT` → 504 - [ ] `INTERNAL` → 500 - [ ] Operation-level code with `http_status` → declared status - [ ] Operation-level code without `http_status` → 500 - [ ] `HTTP_`-prefixed codes (from `from_openapi`) → the status number - [ ] `call_error_to_http_response(error)` builds an `axum::response::Response` with the status + JSON body - [ ] `retryable: true` on `503`/`429`-class errors → `Retry-After` header (when value present) - [ ] `call_error_to_http_status_with_identity(error, identity)` for the 401/403 split - [ ] Unit test: each protocol code maps to the correct status - [ ] Unit test: operation-level code with `http_status` maps to declared status - [ ] Unit test: operation-level code without `http_status` maps to 500 - [ ] Unit test: `HTTP_404` code maps to 404 (not collided with protocol `NOT_FOUND`) - [ ] Unit test: `FORBIDDEN` with `None` identity → 401 - [ ] Unit test: `FORBIDDEN` with `Some(identity)` → 403 - [ ] `cargo test -p alknet-http` succeeds - [ ] `cargo clippy -p alknet-http --all-targets` succeeds with no warnings ## References - docs/architecture/crates/http/http-server.md — Error Mapping table (§"Error Mapping") - docs/architecture/crates/http/http-adapters.md — Error Fidelity (§"Error Fidelity (ADR-023)") - docs/architecture/decisions/023-operation-error-schemas.md — ADR-023 (protocol/operation codes distinct, HTTP_ prefix) ## Notes > The mapping is a two-way-door default (the exact status for ambiguous > codes can be refined additively); the one-way constraint is that > protocol-level and operation-level codes are distinct (ADR-023) and > from_openapi-imported codes are prefixed HTTP_. The FORBIDDEN > case needs the caller's identity state to distinguish 401 (no token) > from 403 (token present but insufficient scopes). This task is the > runtime HTTP response mapping; the to_openapi doc-level error > projection is the to-openapi task, and the from_openapi error import > is the from-openapi task. ## Summary > To be filled on completion