TL; DR
- A common middleware pattern with the
net/http
package cannot handle outgoing responses - A delicate approach to manipulating
Handler
is necessary, but it is still fairly limited
So, we all know how to write middleware with Go's net/http
?
After learning and playing around with the net/http
package for a while (of course, with some help from books and other resources), I was pretty satisfied with how I came up with a way to write middleware in Go. It was elegant and very straightforward, as follows:
func AddSomeMiddleware(
handler http.Handler,
someFunc func(r *http.Request),
) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// do something right before the handler handles the request
someFunc(r)
// the handler that this middleware wraps around
handler.ServeHTTP(w, r)
})
}
The function above takes a handler and a function to be invoked before the handler, and returns a new handler by simply composing them with http.HandlerFunc
.
If you search for how to write middleware in Go, I would say about 7 or 8 out of 10 results will show exactly the same pattern as above. I thought this pattern serves exactly what we need from middleware when writing server applications.
Middleware of net/http
is only half middleware
However, I ran into a problem this simple pattern cannot handle. Did you notice? Yes, this middleware cannot touch the response part! Take a look at the code again:
func AddSomeMiddleware(
handler http.Handler,
someFunc func(r *http.Request),
) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
someFunc(r)
handler.ServeHTTP(w, r)
// what about afterwards?
// we want something like postProcess(w)
})
}
Of course AddSomeMiddlware
can still mess with http.ResponseWriter
even after handler.ServeHTTP(w, r)
is invoked. But what if handler
has already written a body that you want to postprocess?
The problem gets trickier when it comes to writing headers, as once ResponseWriter.WriteHeader
has been called:
Changing the header map after a call to ResponseWriter.WriteHeader has no effect unless the HTTP status code was of the 1xx class or the modified headers are trailers.
Since ResponseWriter.Write
calls ResponseWriter.WriteHeader
if it has not been explicitly invoked beforehand, you cannot add, remove, or modify the headers (try yourself!). In short, you lose control over the response part once you hand over http.ResponseWriter
to the handler. net/http
expects http.Handler
to finish handling an entire HTTP request. This extraordinary feature of the net/http
package makes our middleware as half middleware.
Wrapping another http.Handler
is not a solution
We might trust an http.Handler
not to write the response, and wrap it with middleware:
func AddSomeMiddleware(
handler http.Handler,
someFunc func(r *http.Request),
postProcess func(w http.ResponseWriter),
) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
someFunc(r)
handler.ServeHTTP(w, r)
// this postProcess knows nothing about the handler above
postProcess(w)
})
}
Unfortunately, this doesn't resolve the problem at all, since postProcess
knows nothing about what's happening inside handler.ServeHTTP
. What are the headers? What is the status code? Without that information, we cannot send back a response to the client. Also, what about early return? If there is an error inside handler.ServeHTTP
, writing a response afterward is meaningless.
Moreover, there are several useful functions that help write an HTTP response directly to http.ResponseWriter
. For example, http.Error
is very useful to write a succinct error response, and the encode/json
package has Encoder.Encode
that enables writing an application/json
response in a single line.
if err := json.NewEncoder(w).Encode(jsonBody); err != nil {
log.Println(err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
Therefore, it is very tempting to write the response inside handler.ServeHTTP
.
How about writing custom "handlers"?
One way of resolving this issue would be to write our own handlers that don't expose themselves to the raw *http.Request
and http.ResponseWriter
. A mock example could be as follows:
// Controller handles low-level ResponseWriter and Request
func SomeController(w http.ResponseWriter, r *http.Request) {
// parse the raw request
request := parseRequest(r)
// middleware for the incoming request
request = requestMiddleware(request)
response, err := SomeService.Handle(request)
if err != nil {
// early return
log.Println(err)
http.Error(w, "some error message", someErrorCode)
return
}
// middleware for the outgoing response
response = responseMiddlware(response)
// write the response finally
writeResponse(w, response)
}
// custom request
type Request struct {
// ...
}
// custom response
type Response struct {
// ...
}
// custom handler
type Handler interface {
Handle(r Request) Response
}
This may comes at a cost. It could be overwhelming to redesign the Request
and Response
if we want to do it properly. However, once we know what we're doing, it doesn't have to be difficult. You might not use a brand new Request
at all but instead use the plain *http.Request
. Furthermore, the response type could be this simple:
type Response struct {
headers map[string]string
cookies []*http.Cookie
body any
// additional necessary fields
// ...
}
Then what about middleware? One strategy is to divide middleware into two categories:
The first category of middleware directly interact with
*http.Request
, such ashttp.TimeoutHandler
, or the middleware pattern we introduced early in this article. This category provides low-level functionality, such as logging the basic information about the request.The second category is about the service level of the application, such as
requestMiddleware
orresponseMiddleware
functions insideSomeController
in the example code. As long as the service logic functions maintain the functional form offunc Handler(Request) (Response, error)
, then we can easily chain those functions where wrapping functions work as middleware.
func WrappingServiceHandler(r Request) (Response, error) {
r = beforeMiddleware(r)
response, err := WrappedServiceHandler(r)
if err != nil {
// early return
return nil, err
}
response = afterMiddleware(response)
return response, nil
}
Conclusion
Unless we write our own web framework in Go, this half middleware pattern is not easy to overcome. In this article, we suggested a pattern with an abstraction cost. Of course, it doesn't resolve all our problems and might incur several edge case problems.
If we know what we are doing, maybe it is more sensible to repeat the post processing logic right before writing the response inside each of handler.ServeHTTP
. Yes, as usual, it depends...
Top comments (1)
Hi,
Great article, thanks for writing this! It really clicked with me because I’m planning to rewrite my chaos-proxy project in Go for better performance and I ran into the same problem: without copying the response left and right, there is no way to tamper with the response with net/http.
I’ve been looking at Fiber since it makes the middleware pattern feel more natural. For example:
A cleaner way to hook into both sides of the lifecycle.