Stage 7 · Master
Phase 5 — API Gateway
Correlation ID
Generate or honor an incoming X-Request-ID at the gateway, and propagate it to every upstream call so a single grep across gateway, user-service, and auth-service logs reconstructs one request's full path through the system.
Three Services, Three Independent Log Streams, No Shared Key
gatewaymw.RequestID (mounted second since the middleware lesson) generates an identifier scoped to the gateway process and returns it as X-Gateway-Request-ID. It is useful inside gateway-service, but user-service generates a different local ID for the same logical request. Correlating one failure across three log streams by timestamp is exactly the incident-response friction a cross-service correlation ID removes.
This lesson does not replace gatewaymw.RequestID. It adds X-Request-ID, which survives process boundaries and may originate from a client, allowing a mobile error report to correlate with gateway and backend logs while each process retains its own local diagnostic ID.
package httpmw
import (
"net/http"
"github.com/google/uuid"
)
const CorrelationHeader = "X-Request-ID"
// Correlation honors a client-supplied X-Request-ID if present (trusting it
// here is safe — unlike identity headers, a forged correlation ID cannot
// grant any privilege, only make an incident harder to trace, which is the
// client's own problem to live with), or generates one if absent.
func Correlation(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get(CorrelationHeader)
if id == "" {
id = uuid.NewString()
}
r.Header.Set(CorrelationHeader, id)
w.Header().Set(CorrelationHeader, id)
next.ServeHTTP(w, r)
})
}Setting the header on both the outgoing request (r.Header.Set, so it propagates to whatever the proxy forwards) and the response (w.Header().Set, so the client can log the same ID it might need to reference in a support ticket) is what makes this a genuine end-to-end correlation mechanism rather than a server-internal-only convenience.
Why This Requires No Change to proxy.go at All
Because httputil.ReverseProxy forwards the entire incoming request, including any headers already set on it by prior middleware, mounting Correlation before proxy.BuildRouter in the middleware chain is sufficient — the header rides along automatically. No explicit 'copy this header to the outgoing request' logic is needed, unlike the trusted-identity headers in jwt-validation, which are deliberately reconstructed rather than passed through.
Correlation Belongs Ahead of AccessLog, Not After
engine.Use(gin.Recovery())
engine.Use(gatewaymw.RequestID()) // process-local ID
engine.Use(sharedhttpmw.Adapt(gatewaymw.Correlation)) // cross-service X-Request-ID
engine.Use(sharedhttpmw.AccessLog(logger)) // temporary baseline log
engine.Use(sharedhttpmw.Adapt(sharedhttpmw.RequireAuth(tokenVerifier)))
engine.Use(sharedhttpmw.Adapt(ratelimit.PerOrgFairLimiter(rl)))
proxy.BuildRouter(engine, authURL, userURL, logger)The aliases make ownership explicit: gatewaymw is the Gateway's local package; sharedhttpmw is the platform package. The logging lesson replaces the temporary baseline AccessLog with Gateway's richer implementation. Correlation must already have populated the header before either logger runs.
Confirming the ID Survives a Full Gateway-to-Backend Hop
Applied exercise
Decide validation rules for a client-supplied X-Request-ID
Correlation currently accepts any client-supplied X-Request-ID value verbatim, including one a malicious or buggy client could set to something enormous, containing control characters, or identical across millions of unrelated requests.
- Propose a minimal validation rule (e.g., a length cap, a character allowlist) that prevents log injection or storage bloat without rejecting the request outright over a malformed ID.
- State what Correlation should do if a supplied ID fails validation — silently generate a fresh one, or reject the request with an error?
- Explain why rejecting the entire request over a malformed correlation ID would be the wrong tradeoff, given the header's actual purpose.
Deliverable
A short written validation policy with an explicit fallback behavior for invalid input.
Completion checks
- The answer chooses graceful fallback (generate a fresh ID) over hard request rejection, and justifies why.
- The proposed validation rule is concrete (e.g., a specific max length or character set), not vague.
Why is a client-supplied X-Request-ID trusted and forwarded as-is, when a client-supplied X-User-Id header is explicitly stripped and never trusted (jwt-validation lesson)?
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.