// Package httpcommon hosts cross-router HTTP middleware shared by the // Game Lobby Service public and internal listeners. package httpcommon import ( "crypto/rand" "encoding/base32" "net/http" "strings" "galaxy/lobby/internal/logging" ) // RequestIDHeader is the canonical HTTP header used to carry a // caller-supplied request id across service hops. const RequestIDHeader = "X-Request-Id" // requestIDTokenBytes controls the entropy of generated request ids. Eight // bytes produce a 13-character base32 token, well above what is needed to // keep collisions vanishingly rare within any single service's logs. const requestIDTokenBytes = 8 // requestIDMaxLength caps the length of caller-supplied request ids so a // hostile or buggy upstream cannot blow up logs and trace attributes. const requestIDMaxLength = 128 // base32NoPadding mirrors the encoding used elsewhere in the lobby module // (see `internal/adapters/idgen`) so generated ids stay visually similar. var base32NoPadding = base32.StdEncoding.WithPadding(base32.NoPadding) // RequestID is the HTTP middleware that materialises the per-request // `request_id` for downstream loggers. It reads the X-Request-Id header // (case-insensitively); when the header is absent, malformed, or longer // than requestIDMaxLength it generates a fresh token from crypto/rand. // The id is stored on the request context via logging.WithRequestID and // echoed back on the response header. func RequestID(next http.Handler) http.Handler { if next == nil { panic("httpcommon: nil next handler") } return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { requestID := normalizeRequestID(request.Header.Get(RequestIDHeader)) if requestID == "" { requestID = generateRequestID() } writer.Header().Set(RequestIDHeader, requestID) ctx := logging.WithRequestID(request.Context(), requestID) next.ServeHTTP(writer, request.WithContext(ctx)) }) } // normalizeRequestID returns a trimmed copy of value when it satisfies the // per-request constraints, otherwise the empty string. The empty return // signals that the middleware must generate a fresh id. func normalizeRequestID(value string) string { trimmed := strings.TrimSpace(value) if trimmed == "" { return "" } if len(trimmed) > requestIDMaxLength { return "" } for _, r := range trimmed { if r < 0x20 || r == 0x7f { return "" } } return trimmed } // generateRequestID returns a fresh opaque id derived from crypto/rand. // Errors from the random source are vanishingly unlikely; the helper // returns the literal "fallback" on the impossible path so the middleware // remains panic-free. func generateRequestID() string { buf := make([]byte, requestIDTokenBytes) if _, err := rand.Read(buf); err != nil { return "rid-fallback" } return "rid-" + strings.ToLower(base32NoPadding.EncodeToString(buf)) }