Expert · Building a REST API with a database · topic 36 of 46 · 35 min
Middleware: logging, recovery and request IDs
Explanation
Why middleware?
Some things must happen for every request: log the method, path, status and time taken; catch panics so the client gets a clean 500; attach a request ID so logs from one request can be found together; check a login token. Copying that code into 40 handlers is error-prone. Middleware writes it once and wraps all handlers.
The shape
func middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// before: runs on the way in
next.ServeHTTP(w, r)
// after: runs on the way out
})
}Logging with the status code
http.ResponseWriter does not tell you which status the handler wrote. A small wrapper struct embeds the real writer and remembers the code when WriteHeader is called.
type statusRecorder struct {
http.ResponseWriter
status int
}
func (s *statusRecorder) WriteHeader(code int) {
s.status = code
s.ResponseWriter.WriteHeader(code)
}
func logRequests(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(rec, r)
slog.Info("request",
"method", r.Method,
"path", r.URL.Path,
"status", rec.status,
"duration_ms", time.Since(start).Milliseconds(),
)
})
}Recovering from panics
func recoverPanics(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if v := recover(); v != nil {
slog.Error("panic in handler", "panic", v, "path", r.URL.Path,
"stack", string(debug.Stack()))
writeError(w, http.StatusInternalServerError, "internal error")
}
}()
next.ServeHTTP(w, r)
})
}Request IDs through the context
type ctxKey string
const requestIDKey ctxKey = "request_id"
func requestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Request-ID")
if id == "" {
id = newID() // e.g. a random hex string or a UUID
}
w.Header().Set("X-Request-ID", id)
ctx := context.WithValue(r.Context(), requestIDKey, id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func RequestIDFrom(ctx context.Context) string {
id, _ := ctx.Value(requestIDKey).(string)
return id
}Chaining
api := &API{store: store}
var handler http.Handler = api.routes()
handler = logRequests(handler)
handler = requestID(handler)
handler = recoverPanics(handler) // outermost: runs first, catches panics from everything inside
srv := &http.Server{Addr: ":8080", Handler: handler, ReadHeaderTimeout: 5 * time.Second}- Put recovery outermost so it also catches panics in other middleware.
- Auth middleware returns early (401) without calling next when the token is invalid.
- Router libraries such as chi provide ready middleware (logger, recoverer, request ID, timeouts) with the same func(http.Handler) http.Handler shape, so you can mix them with your own.
- For CORS (letting a browser app on another domain call your API), set the Access-Control-Allow-* headers in a middleware, or use a well-known library; allow only the origins you trust.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Middleware: logging, recovery and request IDs · 1/7
Middleware adds behaviour to every request: logging, panic recovery, request IDs, auth.
Prefer a video by a teacher? Find free videos on “golang http middleware logging recovery chain tutorial” on YouTube ↗
In real life: Tracing a failed food order across logs
A customer complains that order 5531 failed. Support pastes the X-Request-ID shown in the app's error screen into the log search. Because every log line for that request carries the same request_id, the developer sees the whole story in seconds: the request arrived, the restaurant service timed out after 3 s, and the API returned 504.
func handler(w http.ResponseWriter, r *http.Request) {
log := slog.With("request_id", RequestIDFrom(r.Context()))
log.Info("placing order", "order_id", 5531)
// ... call restaurant service with r.Context() ...
log.Warn("restaurant service timeout", "after_ms", 3000)
writeError(w, http.StatusGatewayTimeout, "restaurant did not respond, please retry")
}
// {"level":"INFO","msg":"placing order","request_id":"7f3a9c","order_id":5531}
// {"level":"WARN","msg":"restaurant service timeout","request_id":"7f3a9c","after_ms":3000}
// {"level":"INFO","msg":"request","method":"POST","path":"/v1/orders","status":504,"duration_ms":3004}Practice (0/4 done)
What is the usual type of Go HTTP middleware?
handler = A(B(C(mux))). Which middleware sees the request first?
An auth middleware finds a valid token but the client gets an empty 200 response. Most likely bug?
Write a requireAPIKey middleware that reads the X-API-Key header and compares it with a key loaded from an environment variable. Return a 401 JSON error if it is missing or wrong, otherwise call next. Test both cases with httptest.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Databases with database/sqlBuilding a REST API with a database · same module
- Configuration, project structure and graceful shutdownBuilding a REST API with a database · same module
- Middleware: checks before your handlerBackend Development · Node.js and Express
- Protecting pages and API routesFull-Stack Development · Authentication and files end to end
- Configuration, secrets and the middleware pipelineC# and .NET · Web APIs with ASP.NET Core and EF Core
- Validating input and handling errors on the serverFull-Stack Development · Building the backend
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
Why wrap http.ResponseWriter in a logging middleware?
Answer: To remember the status code the handler wrote
ResponseWriter has no method to read back the status.
Where should panic-recovery middleware go in the chain?
Answer: Outermost
Outermost catches panics from all inner middleware and handlers.
What is a good use of context.WithValue in middleware?
Answer: Carrying a request ID for this request
Request-scoped data like IDs or the authenticated user.
Why compare secrets with crypto/subtle.ConstantTimeCompare?
Answer: Its timing does not reveal how many characters matched
A normal == can leak information through response timing.
Where should the error-handling middleware be added?Related
Answer: After all routes, at the end
Errors from routes travel down the chain, so the error handler must come after them.
What does net/http do if one of your handlers panics?Related
Answer: It recovers, logs the panic and closes that connection
The HTTP server recovers handler panics so one request cannot kill the server.
What does auth middleware usually do when the session is valid?Related
Answer: Sets req.userId and lets the handler run
Middleware attaches the user id from the session to the request, then calls next() so the route handler runs.
What happens if a middleware does not call next?Related
Answer: The request stops there (short-circuit) and later middleware and endpoints do not run
Short-circuiting is used for things like returning cached responses.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 2 more topics in “Building a REST API with a database” before its exam: Databases with database/sql, Configuration, project structure and graceful shutdown.
- Module 8 exam: Building a REST API with a database · 14 questions · 25 min, without help. 4 topics of this module still to finish first.
- Then the next module, CLI tools and Go in cloud and DevOps. 46 topics left in the course before the final exam.