fix(backend): report ingest correctness — propagate storage failure, 413 on oversize, global body cap (closes #23)
check / check (push) Failing after 1s

A buffer failure on POST /api/v1/reports now returns 500 instead of a
false {"status":"ok"}, so clients can retry. Decode errors are split:
an over-limit body returns 413 (via errors.As on *http.MaxBytesError),
malformed JSON stays 400. A new MaxBodyBytes middleware caps every
route (1 MiB default; rejects an oversized Content-Length up-front and
caps the read otherwise), replacing the per-route reader so the health
check and future routes are bounded too. The raw attacker-controlled
geo blob is no longer logged — only its byte length — and client_id and
timestamp are length-bounded before logging. A decodeJSON handler helper
is added per the HTTP server conventions. Panic recovery is now a local
middleware that routes the stack through slog as structured JSON rather
than chi's plain-text stderr. Error bodies still leak nothing internal.

Chosen failure code for a storage failure: 500, since a full buffer or
write error is server-side and retryable, not the client's fault.

Model: opus-4-8
This commit is contained in:
2026-09-21 13:17:59 +00:00
parent f3895789d2
commit 95c6ec16b4
9 changed files with 433 additions and 33 deletions
+10 -2
View File
@@ -7,18 +7,26 @@ import (
"github.com/go-chi/chi/v5/middleware"
)
const requestTimeout = 60 * time.Second
const (
requestTimeout = 60 * time.Second
// maxRequestBodyBytes caps every request body. A route that
// needs a different bound mounts s.mw.MaxBodyBytes with its
// own value on its group.
maxRequestBodyBytes int64 = 1 << 20 // 1 MiB
)
// SetupRoutes configures the chi router with middleware and
// all application routes.
func (s *Server) SetupRoutes() {
s.router = chi.NewRouter()
s.router.Use(middleware.Recoverer)
s.router.Use(s.mw.Recoverer())
s.router.Use(middleware.RequestID)
s.router.Use(s.mw.Logging())
s.router.Use(s.mw.SecurityHeaders())
s.router.Use(s.mw.CORS())
s.router.Use(s.mw.MaxBodyBytes(maxRequestBodyBytes))
s.router.Use(middleware.Timeout(requestTimeout))
s.router.Get(