Routing, middleware, logging, errors
a small httpx package: JSON write/read helpers, RFC 7807
problem+json errors, a middleware chain (request id, structured access log, panic recovery, rate limit), all wired into the server from Lab 1.
Concepts — Go's http.Handler interface, middleware as func(http.Handler) http.Handler, context.Context value passing, closures over shared state.
Idiom note: middleware in Go is just a function that wraps a handler and returns a handler. No decorators, no framework magic. You compose them by nesting. Understanding this once means you never need a web framework.
Build
internal/httpx/respond.go:
package httpx
import (
"encoding/json"
"net/http"
)
func JSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v) // ⟵ YOU: should an encode error be logged? decide & justify
}
// Problem is RFC 7807. detail must NEVER contain internals or PII (02-security §9).
type Problem struct {
Type string `json:"type"`
Title string `json:"title"`
Status int `json:"status"`
Detail string `json:"detail,omitempty"`
TraceID string `json:"trace_id"`
}
func Error(w http.ResponseWriter, r *http.Request, status int, detail string) {
// ⟵ YOU: pull trace id from context (set by middleware below),
// set Content-Type application/problem+json, write the Problem.
}internal/httpx/middleware.go
internal/httpx/middleware.go:
package httpx
import (
"context"
"log/slog"
"net/http"
"time"
"github.com/google/uuid" // ⟵ YOU: go get this
)
type ctxKey int
const traceIDKey ctxKey = 0
func TraceID(r *http.Request) string { v, _ := r.Context().Value(traceIDKey).(string); return v }
func WithTraceID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := uuid.NewString()
w.Header().Set("X-Trace-Id", id)
ctx := context.WithValue(r.Context(), traceIDKey, id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func AccessLog(logger *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rec := &statusRecorder{ResponseWriter: w, status: 200} // ⟵ YOU: implement this type
next.ServeHTTP(rec, r)
logger.Info("request",
"method", r.Method, "path", r.URL.Path,
"status", rec.status, "dur_ms", time.Since(start).Milliseconds(),
"trace_id", TraceID(r),
// ⟵ YOU: NEVER log query params or body — could carry phone/OMANG. Why?
)
})
}
}
// ⟵ YOU: Recover(logger) — recover() from panics, log with trace id, return 500 problem.
// ⟵ YOU: RateLimit(...) — token-bucket per client IP. golang.org/x/time/rate. Auth routes only.Research checkpoints
you can't read the status back off an http.ResponseWriter. Wrap it. (Search "go http middleware capture status code".) Bonus: why is Hijack/Flush pass-through sometimes needed?
why an unexported ctxKey int type rather than a string? (Search "go context key collision".)
token bucket: what do Limit and Burst mean? Where do per-IP limiters get stored, and how do you stop that map growing forever?
does Recover middleware catch a panic in a goroutine you spawned inside a handler? (No — and know why.)
Verify
- Hitting an error path returns
application/problem+jsonwith a
trace_id that matches the X-Trace-Id header and a log line.
- A handler that
panics returns 500, logs once, server stays up. - 11 rapid hits to an auth route → the 11th gets 429.
- No request body or query string ever appears in logs (grep to confirm).