[proposal]: centralized error logging middleware
Problem Statement: Currently, using go-chi/chi must manually log client errors (4xx) and server errors (5xx) in individual handlers or passing it into a context or helper functions. This leads to:
- Repetitive code: Copy-pasting logging logic across handlers.
- Inconsistent logging: Risk of mismatched log levels (e.g., logging client errors as error instead of warn).
- Loss of context: Errors logged without request-specific data (e.g., trace ID, path, method).
A middleware solution would centralize error logging, enforce consistency, and simplify handler code.
Proposed Solution: Add a built-in error classification middleware to Chi that:
- Automatically logs client errors (4xx) as warnings (slog.Warn).
- Logs server errors (5xx) as errors (slog.Error).
- Enriches logs with request context: (Trace ID, HTTP method, Path ,Status code, Error message)
How It Works: Extend Chi’s WrapResponseWriter to pass errors by adding a method called CaptureErr. Middleware: Post-request, classify errors based on status code and log them.
type ResponseWriter interface {
http.ResponseWriter
Status() int
BytesWritten() int
CaptureErr(err error) // <-- New method
Tee(io.Writer)
Unwrap() http.ResponseWriter
}
Hi, how would this look like when used from within HTTP handlers? Can you provide an example, please?
Have you ever looked into https://github.com/go-chi/httplog?
Glad you asked, inside an http handler:
err := someOperation()
if err != nil {
// Capture the error for logging middleware should be in a helper function)
if ww, ok := w.(middleware.WrapResponseWriter); ok {
ww.CaptureErr(err)
}
w.WriteHeader(http.StatusInternalServerError)
return
}
For the middlware we can add this in httplog or create our own middlware if we use another logger:
defer func() {
status := ww.Status()
switch {
case status >= 500:
log.ErrorContext(r.Context(), "server error",
"err": ww.Err(),
...
)
case status >= 400:
log.WarnContext(r.Context(), "client error",
"err": ww.Err(),
...
)
}
}()
next.ServeHTTP(ww, r)
Yes, I looked into httplog, the difference here is passing the error to the WrapResponseWriter so it could be used in a middleware, this will prevent us to log on each exit point inside a handler, I really try to find a very clean approach for that, what do you think?
Hi,
- With
github.com/go-chi/httplog/v3package, you can now log the error as part of the request log:
import "github.com/go-chi/httplog/v3"
func SomeHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if err := someOperation(); err != nil {
httplog.SetError(ctx, err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
-
If you're looking for a more centralized way of handling errors without http.Handler boilerplate, please have a look at https://github.com/webrpc/webrpc.
Here's an example RPC method returning error. You can do logging/error inspection in one place across the codebase.
-
We're unlikely to merge the
CaptureErr(err error)method to themiddleware.ResponseWriter.
Best, Vojtech