I wrote an indepth blog post about understanding how HTTP works in GO and in that post, i introduced the concept of middleware with the hope of writing about them in the future. Well, welcome to the future.

Middleware

What is a middleware ?

Middleware is computer software that provides services to software applications beyond those available from the operating system. – Wikipedia

While that is specific to an operating system, this concept has being applied in HTTP and it has been proven to work pretty much well.

In a web app, Middleware is a piece of code that sits between a HTTP request and a HTTP response.

Middleware can be run after a response.

For the most parts, we need to run some common operations on a set of routes. Say for instance, we have a {moniker}/profile/* route,

We obviously don’t want a non-logged in user to access that route.

We don’t want Peter to be able to visit sensitive routes like john/profile/delete .

Middleware can also work for stuffs like logging the current request (request ID ?) or adding X-Retry-After HTTP headers to a response. Technically anything.

When Middleware is added to a web application, HTTP flow looks like this =>

Multiplexer

Middleware handler

App handler (say PostUser )

) Middleware handler (not necessary, can skip this)

Making Middleware

A primer to this section would be understanding how HTTP works in Golang.. Here is one to level up

A middleware is nothing more than a regular HTTP handler that filters out requests and dispatch them (HTTP requests) to children handlers.

I called them Children handlers since they usually come in form of a chain.. FirstMiddleware(SecondMiddleware(PostUser())) where PostUser is the handler that actually registers a user into the application.

Let’s have a look at a very basic example of a Middleware. Say we are building an API, we have to return the correct HTTP headers. We can build a middleware that makes sure all our routes return an HTTP content type of application/json .

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package main import ( "fmt" "net/http" ) func main () { http . Handle ( "/me" , json ( http . HandlerFunc ( myHandler ))) http . ListenAndServe ( ":8000" , nil ) } func json ( h http . Handler ) http . Handler { return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) { w . Header (). Set ( "Content-Type" , "application/json" ) h . ServeHTTP ( w , r ) }) } func myHandler ( w http . ResponseWriter , h * http . Request ) { fmt . Fprint ( w , `{"name" : "John Doe"}` ) }

While this is a silly example, json is actually a middleware. It applies the correct content type to responses.

A real world example

Let’s build a middleware that would protect a route from users without a valid api token. This would be very simple, the user just needs to pass in the token via the Authorization: Bearer xxx Headers, then we parse out the value, check if it is a valid token. If it is, we authenticate the user to view his profile. Requests with an invalid Bearer token would error out.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package main import ( "context" "encoding/json" "fmt" "net/http" "strings" "time" ) var users [] * User type User struct { Moniker string `json:"moniker"` Token string `json:"token"` About string `json:"about"` } func init () { t := time . Now (). Add ( time . Minute * - 10 ) fmt . Println ( t ) users = [] * User { { "horus" , "abc123" , "I am Horus" }, { "zeus" , "456789" , "god of all gods" }, } } func main () { http . Handle ( "/profile" , jsonHandler ( profile ( http . HandlerFunc ( profileHandler )))) http . ListenAndServe ( ":8000" , nil ) } func jsonHandler ( h http . Handler ) http . Handler { return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) { w . Header (). Set ( "Content-Type" , "application/json" ) h . ServeHTTP ( w , r ) return }) }

The jsonHandler is from the previous example. It just happened to be renamed. The app handler for the profile route is profileHandler , we don’t have that yet. So we write it.

Remember

The flow is HTTP Mux jsonHandler (middleware) profile (middleware) profileHandler (app handler)

Only users with a valid token should be allowed in

Once a user is logged in, a response containing his/her profile would be sent.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 func profile ( next http . Handler ) http . Handler { return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) { var token string bearer := r . Header . Get ( "Authorization" ) if len ( bearer ) > 7 && strings . ToUpper ( bearer [ 0 : 6 ]) == "BEARER" { token = bearer [ 7 :] } if user , err := findUserByToken ( token ); err == nil { //If the token is valid, save the user profile to the request context ctx := context . WithValue ( r . Context (), "user" , user ) next . ServeHTTP ( w , r . WithContext ( ctx )) return } fmt . Fprintf ( w , `{"error" : "Invalid token", "code" : %d }` , http . StatusUnauthorized ) return }) } func profileHandler ( w http . ResponseWriter , r * http . Request ) { //If we get here, it was a valid request u , ok := r . Context (). Value ( "user" ).( * User ) //Type assert to be sure if ! ok { panic ( "An error occurred while trying to fetch the profile of the user" ) //Real world probably does not need to panic } b , _ := json . Marshal ( u ) //Handle errors in real life fmt . Fprint ( w , string ( b )) } func findUserByToken ( token string ) ( * User , error ) { var activeUser * User for _ , v := range users { if v . Token == token { return v , nil } } return activeUser , fmt . Errorf ( "User with token, %s not found" , token ) }

To test this, we need to run curl -H "Authorization: Bearer abc123" http://localhost:8000/profile -i (our sample user, horus has his token as abc123).

Ok, that works.. Let’s try an invalid token, say (abc12).. curl -H "Authorization: Bearer abc12" http://localhost:8000/profile -i

Closing remarks

While we have manually tested our sample app, automated testing is the way to go… Check out this blog post on testing handlers (middleware and/or application handlers)

Please enable JavaScript to view the comments powered by Disqus.

Disqus