Stage 7 · Master
Phase 5 — API Gateway
Middleware
Fix the exact sequence — Recoverer, RequestID, AccessLog, JWT verification, rate limiting, then routing — as a hard architectural rule, and explain precisely why reordering any two of them reopens a real vulnerability.
Middleware Order Is the Security Boundary, Not an Implementation Detail
Every backend service since user-service's bootstrap has used the sequence Recoverer → RequestID → AccessLog → (auth) → handlers. gateway-service inherits that same convention and extends it with one more stage — rate limiting — placed in a specific position for a specific reason. Because gateway-service is the one process every external request passes through, getting this order wrong here has platform-wide consequences that a single backend service reordering its own middleware would not.
package httpmw
import (
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// RequestID identifies this gateway process's handling of the request. The
// correlation lesson later adds a separate ID that crosses service boundaries.
func RequestID() gin.HandlerFunc {
return func(c *gin.Context) {
id := uuid.NewString()
c.Header("X-Gateway-Request-ID", id)
c.Set("request_id", id)
c.Next()
}
}engine := gin.New()
engine.Use(gin.Recovery()) // 1. contain panics
engine.Use(gatewaymw.RequestID()) // 2. establish request identity
engine.Use(sharedhttpmw.AccessLog(logger)) // 3. log every outcome
engine.Use(sharedhttpmw.Adapt(sharedhttpmw.RequireAuth(tokenVerifier))) // 4. verify identity
engine.Use(sharedhttpmw.Adapt(ratelimit.PerOrgFairLimiter(rl))) // 5. charge the verified tenant
proxy.BuildRouter(engine, authURL, userURL, logger)Import github.com/hoa-platform/backend/pkg/httpmw as sharedhttpmw and services/gateway-service/internal/httpmw as gatewaymw throughout main.go. Gateway owns only its process-local request ID; the shared package supplies AccessLog and the tested Adapt bridge instead of duplicating them.
Why Recoverer Must Be Outermost
A panic anywhere below Recoverer — a nil-pointer dereference in a rarely-hit code path of the JWT verifier, say — would otherwise crash the entire gateway process, taking down every in-flight request across every backend service simultaneously. Recoverer sitting outermost guarantees a single bad request degrades to one 500 response, never a platform-wide outage.
Why JWT Verification Must Precede Rate Limiting, Not Follow It
The rate-limiting lesson introduces a per-organization token bucket — a request's budget is charged against its org_id, extracted from the verified JWT. If rate limiting ran before authentication, the gateway would have no trusted org_id yet to charge against, and would be forced to fall back to a coarser IP-based limit for every request, including already-authenticated ones. Worse, an unauthenticated flood of requests with no valid token would consume rate-limit budget meant for legitimate, verified tenants before ever being rejected by auth — an easy denial-of-service vector against the very defense meant to prevent one.
| Order | Consequence |
|---|---|
| JWT verification, then rate limiting (this platform's choice) | Only requests with a valid, verified org_id consume that org's fair-share budget; forged or missing tokens are rejected by RequireAuth before any budget is spent |
| Rate limiting, then JWT verification | An attacker with no valid token at all can still exhaust IP-level or global rate-limit budget, potentially starving legitimate authenticated traffic before authentication even runs |
The Authentication module already established that RequireAuth must run after AccessLog (so rejected requests are still logged) and before any protected route group. The one new fact this lesson adds is where rate limiting sits relative to that existing sequence — strictly after authentication, never before — and that ordering is specific to a component (a shared, contended rate-limit budget) that individual backend services never had to reason about on their own.
A Test That Fails Loudly If Anyone Reorders This
func TestMiddlewareOrder_UnauthenticatedFloodDoesNotConsumeRateLimitBudget(t *testing.T) {
rl := ratelimit.NewInMemoryForTest(1) // budget of exactly 1 request for this org
router := buildTestRouter(rl)
// Ten requests with NO Authorization header at all.
for i := 0; i < 10; i++ {
resp := doRequest(router, http.MethodGet, "/api/v1/users/me", "")
if resp.StatusCode != http.StatusUnauthorized {
t.Fatalf("request %d: status = %d; want 401 (rejected by auth before rate limiting)", i, resp.StatusCode)
}
}
// A single legitimately authenticated request afterward must still
// succeed — proving the unauthenticated flood above never touched the
// org's rate-limit budget.
resp := doRequest(router, http.MethodGet, "/api/v1/users/me", validTestToken())
if resp.StatusCode == http.StatusTooManyRequests {
t.Fatal("legitimate request was rate-limited; unauthenticated flood must have consumed budget it should never have touched")
}
}This test encodes the exact architectural claim this lesson makes — if a future refactor accidentally swapped the RequireAuth and rate-limit middleware lines, this test (not a code review, which might miss a two-line reorder) is what catches it.
Confirming the Order in a Running Gateway
Applied exercise
Decide where correlation-id propagation fits in this exact ordering
The correlation-id lesson later in this module introduces a middleware that generates or honors an incoming X-Request-ID and propagates it to upstream calls — a fifth concern to slot into the five-stage order already established here.
- State where correlation-id middleware belongs relative to gatewaymw.RequestID — are the process-local and cross-service identifiers the same concern, or genuinely different?
- Justify whether correlation-id propagation needs to run before or after RequireAuth, given that AccessLog (which needs the correlation ID) already runs before RequireAuth.
- State whether correlation-id middleware could reasonably run after rate limiting instead of before, and what would break if it did.
Deliverable
A short written placement decision for correlation-id middleware within the existing five-stage order.
Completion checks
- The answer distinguishes the gateway's process-local request ID from the client-facing X-Request-ID correlation concern.
- The placement decision keeps correlation-id available to AccessLog, consistent with AccessLog's existing position in the order.
Why must JWT verification run before rate limiting in the gateway's middleware chain, rather than after?
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.