Exploring Middlewares in Go
I came across “Middlewares” for writing HTTP servers originally in the Node.js ecosystem. There is this beautiful library called express which sparked the joy of middleware in me. In case you haven’t heard of middleware before, I think you should read this beautiful page from expressjs documentation to get a taste of them. (I genuinely feel that it is the best possible introduction for middleware, hence opening up the post with it)
With enough JavaScript for the day, we will jump into Go now. 😅
My goal for this post is to understand how to {use, write} middlewares in Go HTTP servers. We will also try to search the internet and surface some Go middlewares that we can add to our day-to-day toolkit.
Problem
Let us take a simple problem and work our way upwards. Here is the problem statement:
Write an HTTP server that contains multiple routes. When a request is made to a route, print a log line at the start and the end of the request. Something like
1 | 2024/05/21 00:49:32 INFO start method=GET path=/one |
Solution
Without Middleware
A solution without using middleware would look like
1 | package main |
How do we avoid copy-pasting those two lines to every HTTP handler function? Middlewares for the win!
Basic Middleware
1 | package main |
Using http.HandleFunc
We are not done yet! There is still room for improvement. Notice how big the method signature for logRequest
is! we can start from there. I remember a standard library type called http.HandlerFunc
which could be used in the place of func(ResponseWriter, *Request)
. If we start using it, our middleware looks like this.
1 | func logRequest(next http.HandlerFunc) http.HandlerFunc { |
While browsing through the Go docs, I noticed that http.HandleFunc
has the below method signature.
1 | func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) |
That raised a question in me. Why don’t they use func HandleFunc(pattern string, handler http.HandlerFunc)
instead? I thought http.HandlerFunc
is an alias type for func(ResponseWriter, *Request)
. Digging through the standard library source code had the answer. It seems like it is just not a simple alias, but more than that. Copy pasting the implementation of http.HanderFunc
for you straight out of Go source :D
1 | // The HandlerFunc type is an adapter to allow the use of |
oh wow, so http.HandlerFunc
is a func(ResponseWriter, *Request)
which implements the http.Handler interface.
Enter http.Handler
Why would we need an adapter like http.HandlerFunc
that implements the http.Handler
interface. To understand, let us take a look at the interface definition.
1 | type Handler interface { |
and also read through the http.Handler documentation. At first, it didn’t solve my doubt, but then I discovered this beautiful example in the docs. Copy pasting the example from the docs here for you to have a quick look.
1 | package main |
wow, did you get it? Sometimes your handler is more than just a func(http.ResponseWriter, *http.Request)
. It could be a struct that contains data that could be used in your request logic. Like in the above case, countHandler
maintains a counter protected by a mutex. Each and every request to /count
would increment the counter atomically.
For simple routes, which are just a bunch of instructions we could use http.HandleFunc
. But once your handler gets complex, like having to maintain data that is common to all requests of the handler, then move upward and go for http.Handle
.
woah, this just cleared my long-standing doubt about “when to use http.Handle
and http.HandleFunc
?”
It is getting a bit clearer now on why the http.Handler
interface is needed. With two ways of defining a HTTP handler: one being to write a func(http.ResponseWriter, *http.Request)
and pass it to http.HandleFunc
and another being to write a struct with the necessary logic and pass it down to http.Handle
function, the standard libary needs a common ground in which all its methods can operate on both the types of handlers. Hence an interface.
http.HandlerFunc to http.Handler
Now that it is evident that a Go programmer could choose between using http.Handle
or http.HandleFunc
to serve their handlers, it is necessary that any HTTP middleware should work for both of those use cases. With the current approach to our solution, we will only support middlewares that are input to http.HandleFunc
. Hence moving our middleware to use http.Handler
interface, that way we could accommodate both types of handlers.
1 | package main |
Standard library Middlewares
The net/http
package in the standard library of Go contains middlewares. If you haven’t realized it yet, don’t worry. That is because they don’t advertise those functions as “middleware” (ctrl+f on docs for middleware leaves you with 0 matches :D)
AllowQuerySemicolons
1 | func AllowQuerySemicolons(h Handler) Handler |
TIL that we could use semicolons instead of ampersands in query strings (though this style is deprecated by W3C). Read more about it here: https://github.com/golang/go/issues/25192. This middleware is present in the stdlib for solving that problem by replacing the ;
with &
under the hood.
MaxBytesHandler
1 | func MaxBytesHandler(h Handler, n int64) Handler |
This could be used to limit the acceptable request body size. Under the hood, it uses MaxBytesReader
:
MaxBytesReader prevents clients from accidentally or maliciously sending a large request and wasting server resources. If possible, it tells the ResponseWriter to close the connection after the limit has been reached.
StripPrefix
1 | func StripPrefix(prefix string, h Handler) Handler |
The docs says
StripPrefix returns a handler that serves HTTP requests by removing the given prefix from the request URL’s Path (and RawPath if set) and invoking the handler h.
My first impression is how could this be useful. Oh, wait for the blast! Here we go once again with a beautiful copy-paste of an stdlib example.
1 | package main |
TimeoutHandler
1 | func TimeoutHandler(h Handler, dt time.Duration, msg string) Handler |
As the name says, it times out the handler if the request is taking more than the given duration.
Third-party Middlewares
I came across this beautiful library called chi
which comes loaded up with a bunch of middlewares out of the box: https://github.com/go-chi/chi?tab=readme-ov-file#middlewares
I would suggest starting with the default chi recommendation:
1 | // A good base middleware stack |
and then build up the chain. Go explore and catch ‘em all!
(also let me know your favorite middleware if you have one - because I am trying to discover more third-party middlewares in Go)
Communicate
When writing or using middleware, you may need to pass down a variable that was created by one middleware into another middleware or in the request handler. In the case of JS, we would just mutate the request
object directly since it is dynamically typed :D (lol, good old days). In the case of Go, we can’t do that and we will need a way of passing through variables of any type via the available ResponseWriter
or Request
objects.
I have previously written a whole blog post on the pitfalls of context.WithValue and when not to use them. And well, this is actually the use-case where you can use them!
A context variable is available to you in all the middlewares and the handlers via the http.Request
object. We could use that to store and pass down information.
1 |
|
You still need to be careful while using context.WithValue
. What if you miss calling a middleware, but try to look up the value that it is supposed to set in r.Context
? It changes the trajectory of your request during runtime and in the worst case it will lead to runtime panics in your handler. I am wondering if we could somehow catch this kind of stuff during compile time (like maybe by writing a library or perhaps someone already thought about this before - if so, let me know!)
Chain
You might soon end up having to call multiple middleware for your handlers. In that case, your code would look like:
1 | // middlewares for unauthenticated routes |
We need a way to chain the middleware and store the chain so that we can reuse it between handlers. I recently discovered a library for this, which might help here: https://github.com/justinas/alice
1 | unAuth := alice.New(Logger, RequestID) |
You can also use a routing library like chi
where the request middlewares are defined at the router level.
Closing Thoughts
I hope this exploration was useful to you! It definitely made me learn some unexpected things like “when to use http.Handle? when to use http.HandleFunc? ….”. This is also inspiring me to write a small middleware library that I have been thinking about.
~ ~ ~ ~
In an alternate universe, someone declared type Middleware func(Handler) Handler
in net/http
and (use your imagination).