feat(F-003): HTTP foundation with request context and error envelope
- request_id generated or sanitized-propagated on every request (x-request-id)
- structured JSON logging (pino), one correlated line per request, injectable logger
- error envelope v2 { error: { statusCode, code, message, details? }, requestId }
- 5xx messages always generic; stack traces stay in server logs only
- explicit parseJson (zod) input validation hook at the API layer
- README HTTP contract section; deps justified in spec/tech.md
- all gates approved; verify.sh green
This commit is contained in:
54
specs/F-003-http-foundation/DESIGN.md
Normal file
54
specs/F-003-http-foundation/DESIGN.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# DESIGN — F-003 HTTP foundation and request context
|
||||
|
||||
## Architecture
|
||||
All cross-cutting HTTP behavior lives in explicit, readable code:
|
||||
|
||||
- `src/infrastructure/logging/logger.ts` — pino factory. JSON only, level from
|
||||
`LOG_LEVEL` (default `info`), base field `service`. Accepts an optional writable
|
||||
destination so tests can capture output without transports (no magic).
|
||||
- `src/shared/errors.ts` — `AppError` class (statusCode, code, message) and the
|
||||
single `errorEnvelope(statusCode, code, message, requestId)` builder. Envelope v2:
|
||||
`{ error: { statusCode, code, message }, requestId }`.
|
||||
- `src/shared/http-input.ts` — `parseJson(schema, input)` using zod. On failure
|
||||
throws `AppError(400, VALIDATION_ERROR)` carrying field-level issues (field paths
|
||||
only, never internal state).
|
||||
- `src/app/build-app.ts` — composition root wires everything explicitly:
|
||||
- Fastify `genReqId`: trust incoming `x-request-id` only if it matches
|
||||
`^[A-Za-z0-9._-]{1,128}$`; otherwise generate `crypto.randomUUID()`.
|
||||
- `onRequest` hook: set `x-request-id` response header, start timer.
|
||||
- `onResponse` hook: one JSON log line per request
|
||||
`{ requestId, method, url, statusCode, durationMs }`.
|
||||
- `setErrorHandler`: map `AppError` and Fastify errors to the envelope. 4xx expose
|
||||
their message; >=500 always returns generic `Internal Server Error`. Errors are
|
||||
logged with stack server-side, tagged with requestId.
|
||||
- `setNotFoundHandler`: envelope with code `NOT_FOUND` + requestId.
|
||||
- `src/infrastructure/http/server.ts` — startup log lines through the logger.
|
||||
|
||||
## Request id policy
|
||||
- Propagation is opt-in and sanitized; anything suspicious is replaced by a fresh UUID.
|
||||
- Response header name: `x-request-id`.
|
||||
|
||||
## Validation policy
|
||||
- Validation is explicit per route: call `parseJson(schema, body)` inside the handler.
|
||||
- No schema decorators, no magic body binding. The F-003 deliverable is the hook
|
||||
(helper + envelope integration), exercised by composition tests with a test-only
|
||||
route registered on the built app.
|
||||
|
||||
## Backward compatibility
|
||||
- `GET /health` response body unchanged; envelope gains fields (additive).
|
||||
|
||||
## Files
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| src/infrastructure/logging/logger.ts | pino factory |
|
||||
| src/shared/errors.ts | AppError + envelope v2 |
|
||||
| src/shared/http-input.ts | zod parse helper |
|
||||
| src/app/build-app.ts | hooks, genReqId, handlers |
|
||||
| src/infrastructure/http/server.ts | startup logging |
|
||||
| src/app/tests/http-foundation.test.ts | composition tests (request id, logs, envelope, no leak) |
|
||||
| src/shared/tests/http-input.test.ts | helper unit tests |
|
||||
|
||||
## Risks / mitigations
|
||||
- Double logging (fastify internal + hooks): keep `logger: false` on Fastify, log only via explicit hooks.
|
||||
- Header injection through propagated id: strict regex + length cap.
|
||||
- 5xx leaking internals: handler returns fixed generic message for anything >=500; tests assert absence of thrown message/stack.
|
||||
37
specs/F-003-http-foundation/SPEC.md
Normal file
37
specs/F-003-http-foundation/SPEC.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# SPEC — F-003 HTTP foundation and request context
|
||||
|
||||
## Problem
|
||||
Every request needs identity, structured logs and predictable errors. Today the app
|
||||
answers with ad-hoc console logging potential, no request correlation id, and an
|
||||
error envelope without request context.
|
||||
|
||||
## Goal
|
||||
request_id everywhere, JSON logs, one error envelope, no magic.
|
||||
|
||||
## Scope IN
|
||||
- request_id generated or propagated on every request
|
||||
- Structured JSON logging with request_id
|
||||
- Single error response envelope (v2: adds stable `code` + `requestId`)
|
||||
- Input validation hook on API layer (foundation helper, explicit usage)
|
||||
|
||||
## Scope OUT
|
||||
- No tracing backend
|
||||
- No metrics backend
|
||||
- No production endpoints that consume validation yet (first consumer: F-005 identity)
|
||||
|
||||
## Acceptance criteria
|
||||
1. Every response carries `request_id` (via `x-request-id` response header; error bodies also embed it).
|
||||
2. Every log line for a request carries the same request_id.
|
||||
3. Given an invalid request When handled Then the error envelope shape is stable.
|
||||
4. Internal stack traces never leak to API responses (5xx message is generic; stack stays server-side in logs).
|
||||
5. `./scripts/verify.sh` green.
|
||||
|
||||
## Non-functional
|
||||
- JSON-only logs (no pretty printers in runtime).
|
||||
- Incoming `x-request-id` must be sanitized before reuse (length/charset limits).
|
||||
- No new framework magic: logging hooks and error handler are explicit code in the composition root.
|
||||
|
||||
## Dependencies added
|
||||
- pino (JSON structured logging; Fastify ecosystem standard)
|
||||
- zod (explicit input validation; no decorators/annotations magic)
|
||||
Justification goes to `spec/tech.md`.
|
||||
11
specs/F-003-http-foundation/TASKS.md
Normal file
11
specs/F-003-http-foundation/TASKS.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# TASKS — F-003 HTTP foundation and request context
|
||||
|
||||
- [ ] TASK-001 deps: add pino + zod; justify both in spec/tech.md
|
||||
- [ ] TASK-002 src/infrastructure/logging/logger.ts: createLogger({ level?, destination? })
|
||||
- [ ] TASK-003 src/shared/errors.ts: AppError + errorEnvelope v2 (code + requestId)
|
||||
- [ ] TASK-004 src/shared/http-input.ts: parseJson(schema, input) -> AppError(400, VALIDATION_ERROR)
|
||||
- [ ] TASK-005 src/app/build-app.ts: genReqId sanitization, onRequest/onResponse hooks, error + notFound handlers with requestId; buildApp(deps) injectable logger
|
||||
- [ ] TASK-006 src/infrastructure/http/server.ts: logger-based startup lines
|
||||
- [ ] TASK-007 tests: src/shared/tests/http-input.test.ts
|
||||
- [ ] TASK-008 tests: src/app/tests/http-foundation.test.ts (id propagation/sanitization, log correlation, envelope stability incl. malformed JSON, no stack leak)
|
||||
- [ ] TASK-009 run lint + boundaries + typecheck + build + test + verify.sh
|
||||
21
specs/F-003-http-foundation/TESTS.md
Normal file
21
specs/F-003-http-foundation/TESTS.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# TESTS — F-003 HTTP foundation and request context
|
||||
|
||||
## Unit — src/shared/tests/http-input.test.ts
|
||||
1. parseJson returns parsed value for valid input
|
||||
2. parseJson throws AppError 400 VALIDATION_ERROR with field issues for invalid input
|
||||
3. parseJson rejects non-object input when schema expects object
|
||||
|
||||
## Composition — src/app/tests/http-foundation.test.ts
|
||||
(app built with logger writing to an in-memory stream)
|
||||
|
||||
1. GET /health responds 200 with x-request-id header (UUID shape)
|
||||
2. Valid incoming x-request-id is propagated: same value in response header and in the request log line
|
||||
3. Malicious x-request-id (bad charset / >128 chars) is replaced by generated UUID; log line carries the generated id
|
||||
4. Unknown route -> 404 envelope { error: { statusCode: 404, code: NOT_FOUND, message }, requestId } + x-request-id header
|
||||
5. Invalid JSON body on test route -> 400, envelope shape stable, requestId present
|
||||
6. Schema-invalid body on test route -> 400 code VALIDATION_ERROR, issues mention field path only
|
||||
7. Handler throwing Error('secret internal detail') -> 500 generic message, response body contains neither the message nor a stack; error log line contains requestId and stack
|
||||
8. Every request produces exactly one 'request completed' log line carrying the requestId
|
||||
|
||||
## Manual / QA
|
||||
- verify.sh green; live curl shows x-request-id header and JSON envelope on unknown route.
|
||||
Reference in New Issue
Block a user