personal-blog/content/posts/writing-a-golang-middleware...

161 lines
6.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

+++
draft = false
date = 2021-08-01T22:05:04+05:00
title = "Writing a Middleware for your HTTP handler Golang"
description = ""
slug = ""
authors = ["Basit Ali"]
tags = ["golang", "middleware", "http", "handler"]
categories = []
externalLink = ""
series = []
+++
If you are a backend developer working daily with HTTP requests then you have most likely already encountered situations where you want a common functionality across all the incoming HTTP requests, which can be as simple as checking if the `Content-Type` header only has the value `application/json` if you only support json, or maybe you want to spoof your HTTP request to change the method type from `POST`,`GET` or `PUT` to something else based on the `X-HTTP-Method-Override` header, or of course authenticate before finally passing the request to the destination HTTP handler.
![middlewares](/posts/img/2021/middleware-header.png)
You can achieve the following behaviour by writing a `middleware`, also known as a `filter` in some other backend frameworks. You can have as many middlewares as you want, each with a separate responsibility, and can chain them together to funnel incoming HTTP requests.
Writing a `middleware` in Go is pretty simple, you just need to **wrap** your `middleware` around the base HTTP handler, which so to speak is a thin **wrapper** around your HTTP handler.
Lets start with `http` package's `ListenAndServe` method, which listens for incoming connections and serves with the handler to handle the requests, and lets write a handler for root `"/"` path which checks for the header `Content-Type` to see if it's `application/json`, because our API only accepts JSON, and respond with following json `{"msg":"Hello world!"}` to any incoming request:
```go
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", rootHandler)
http.ListenAndServe(":8080", mux)
}
func rootHandler(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Content-Type") != "application/json" {
http.Error(w, "Only application/json content type supported", http.StatusUnsupportedMediaType)
return
}
w.Write([]byte(`{"msg":"Hello world!"}`))
}
```
Of course in reality you could have a dozen of handlers each with a different endpoint, but lets keep it simple for this tutorial.
Now let's assume we have a dozen request handlers in our project and we want each handler to check for `Content-Type` header as we did in our `rootHandler`, we want our code to be DRY *(Don't Repeat Yourself)*.
Which brings us to writing a `middleware` to check the `Content-Type` header for each incoming request. Lets write a middleware and call it `wrap`:
```go
func wrap(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// middleware logic
next.ServeHTTP(w, r)
})
}
```
The `wrap` function takes in a `Handler` and returns a `Handler`, this syntax allows it to be used where we would normally use a request handler, and also allow us to chain multiple middlewares together. Now we can do something like `wrap(handler)` instead of just using `handler`, this would call our `middleware` logic before calling `next.ServeHTTP(w, r)` which can be the next middleware in the chain or a final call to our request handler.
In the following example, we are wrapping our `wrap` middleware around `mux` so every incoming request will pass through our middleware first:
```go
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", rootHandler)
http.ListenAndServe(":8080", wrap(mux))
}
func rootHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"msg":"Hello world!"}`))
}
func wrap(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Content-Type") != "application/json" {
http.Error(w, "Only application/json content type supported", http.StatusUnsupportedMediaType)
return
}
next.ServeHTTP(w, r)
})
}
```
So far we've been using a single middleware, lets see multiple middlewares chained together in action. We can define multiple middlewares with the same signature as of `wrap`, replacing the comment `// middleware logic` with our code.
>```go
> func wrap(next http.Handler) http.Handler {
> return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
> // middleware logic
> next.ServeHTTP(w, r)
> })
> }
>```
Lets rename `wrap` to `enforceJSON`, and write another `middleware` which will log every incoming request's method and path, and chain them both together:
```go
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", rootHandler)
http.ListenAndServe(":8080", logRequest(enforceJSON(mux)))
}
func rootHandler(w ResponseWriter, r *Request) {
w.Write([]byte(`{"msg":"Hello world!"}`))
}
func logRequest(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println(r.Method, r.URL.Path) // logs: GET /v1/users/1234
next.ServeHTTP(w, r)
})
}
func enforceJSON(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Content-Type") != "application/json" {
http.Error(w, "Only application/json content type supported", http.StatusUnsupportedMediaType)
return
}
next.ServeHTTP(w, r)
})
}
```
See how we are using `logRequest(enforceJSON(mux))` instead of `wrap(mux)`? Now every incoming HTTP request will be logged and then checked for `Content-Type` before being passed onto the handler.
A cleaner way to chain multiple handlers is writing a single `wrap` middleware and chain all the middlewares inside `wrap`:
```go
func wrap(handler http.Handler) http.Handler {
handler = enforceJSON(handler)
handler = logRequest(handler)
return handler
}
```
And then instead of:
```go
http.ListenAndServe(":8080", logRequest(enforceJSON(mux)))
```
We can use:
```go
http.ListenAndServe(":8080", wrap(mux))
```
-----------
That's all on middlewares in golang today. Hope it was useful with your understanding on middlewares or how to write them.