Loading
chore(transport): add JSON error envelope (S01 Step 2)
Summary
Introduces internal/transport: the shared JSON error envelope and helpers (WriteJSON, WriteError, ten error-code constants, JSONContentType) per the S01 spec's "Standard error response format" section. Leaf package with no callers on this branch — Step 3 (middleware) and Step 5 (composition root) wire it in.
- Plan: docs/plans/2026-05-20-s01-http-server-and-routing.md — Step 2
- Spec: docs/specs/S01-http-server-and-routing.md — sections "Standard error response format" and "Error codes"
- Issue: closes part of #89 (closed)
Acceptance criteria
| AC | Covered by |
|---|---|
#16 (closed) (envelope body + status + Content-Type: application/json; charset=utf-8) |
TestWriteError_FullEnvelope |
#16 (closed) (stable shape when correlation ID absent — request_id key present, empty value) |
TestWriteError_EmptyCorrelationID |
The remaining 19 ACs from S01 are covered by other steps per the plan's coverage table.
Test plan
-
go test -race -short -count=1 ./internal/transport/...— green -
golangci-lint v2.12.0— 0 issues -
goimports -l— clean - All 10 error-code constants pinned by
TestErrorCodes(one subtest per code, round-tripped throughWriteError) -
JSONContentTypeconstant pinned byTestJSONContentType - Reviewer: confirm the doc comment on
WriteJSONhonestly reflects theany-body trade-off
Reviewability notes
- No callers yet. This is intentional — the package is leaf code.
WriteErrorwill be invoked from Step 3 middleware (404/405 stdlib wrap, request-body-size 413, panic recovery 500) and from a few management-API handlers in Step 5. - Forward notes from `/review-branch`: the package deliberately does not log post-commit Encode errors (no logger on the helper surface). When the request-scoped `*slog.Logger` lands in Step 3 middleware, that is the surface that should report Encode failures (documented in the `WriteJSON` doc comment). The `Envelope` literal escapes to heap via `any` boxing in `WriteError`; acceptable on the error path, revisit only if production profiles show error-path allocations as a top contributor.
- `SafeMessage` type (introduced in `ca58f0d4` after the AppSec MR-reviewer flagged the unconstrained `message string` parameter): `WriteError` now takes `transport.SafeMessage` instead of `string`, so `err.Error()` cannot be passed directly. Callers wrap with `transport.Msg("...")` at the call site, making the spec's "no internal details in ErrorBody.Message" rule a compile-time gate. This also retires the latent edge case from `/review-branch` O1.
- @jdrpereira-driven hardening (commits `949e9ec5` → `cc73b3c9`): six follow-ups on the SafeMessage MR. (a) Plan Status row for Step 2 now records !261 (merged). (b) `//nolint:errchkjson` rationale corrected and the `WriteJSON` doc expanded to enumerate both failure modes (encode-side vs write-side). (c) Package doc no longer presumes the auth middleware uses this envelope (auth is owned by S08). (d) New `TestWriteJSON_EncodeFailure_EmptyBody` pins the documented empty-body-on-encode-failure contract. (e) New `transport.Code` named type retypes the 10 canonical error-code constants and the `WriteError` argument so `code` becomes a compile-time gate, analogous to `SafeMessage` on `Message`. (f) Package doc + `Msg` godoc name `Msg` as the sanctioned `SafeMessage` construction site so the audit story holds (one grep finds every safe-string crossing).
- Squash on merge. The two prior commits had `test(...)` and `chore(...)` types; the squash subject is set to `feat(transport):` so the merged commit on `main` carries the right SemVer / SOX classification.
Related
- Step 1 (
internal/config): !254 (merged) — pipeline green, awaiting human review
Edited by João Pereira