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.

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 through WriteError)
  • JSONContentType constant pinned by TestJSONContentType
  • Reviewer: confirm the doc comment on WriteJSON honestly reflects the any-body trade-off

Reviewability notes

  • No callers yet. This is intentional — the package is leaf code. WriteError will 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.
  • Step 1 (internal/config): !254 (merged) — pipeline green, awaiting human review
Edited by João Pereira

Merge request reports

Loading
Loading