Changing the middleware behavior with mux
Tags: go, middleware
Published: 2021-03-20 00:20:49

**TL;DR**: You can change the behavior of a middleware exchanging it for another previously stored.


Probably you have used [gorilla/mux](https://github.com/gorilla/mux).

Something that I like about mux is the possibility to add middlewares to it. You can find the description and some examples of that in the [repository](https://github.com/gorilla/mux#middleware). 

[Here](https://github.com/juanpabloaj/stateful_middleware_example/blob/17cb8f24a71abd7a2e49e03c5da727b2aaa6a68f/main.go) a middleware example based on the documentation.

```go
package main

import (
	"encoding/json"
	"log"
	"net/http"
	"time"

	"github.com/gorilla/mux"
)

func loggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		log.Println(r.RequestURI)

		defer func(startedAt time.Time) {
			log.Println(r.RequestURI, time.Since(startedAt))
		}(time.Now())

		next.ServeHTTP(w, r)
	})
}

func home(w http.ResponseWriter, r *http.Request) {
	json.NewEncoder(w).Encode(map[string]bool{"ok": true})
}

func main() {
	r := mux.NewRouter()
	r.HandleFunc("/", home)
	r.Use(loggingMiddleware)
	log.Fatal(http.ListenAndServe(":8080", r))
}
```

In that example, every request will pass through the middleware and it will show the request's URI and the request's time duration in the logs.

```
$ curl 0.0.0.0:8080?bar=1
```

```
2021/03/19 19:55:17 /?bar=1
2021/03/19 19:55:17 /?bar=1 17.395µs
```

Thanks to the middleware `loggingMiddleware`, you can know which requests are calling your service.

Sometimes I have needed to change the behavior to middlewares similar to the previous one. For example, do more or fewer actions. To try to explain that idea I will show another simpler middleware.

```go
func noVerboseMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		log.Println(r.RequestURI)
		next.ServeHTTP(w, r)
	})
}
```

The middleware `noVerboseMiddleware` shows less information, it shows the URI only once, at the beginning of the request, without the second log line with the request's time duration.

```
$ curl 0.0.0.0:8080?bar=1
```

```
2021/03/19 20:39:12 /?bar=1
```

Similar to the previous one but less verbose.

I would like to exchange the behavior of them, without rebuilding the code or restart the service. For that, I will create a struct to store a map of middleware. It will give us the possibility to exchange them.


```go
type statefulMiddleware struct {
	current     func(http.Handler) http.Handler
	middlewares map[string]func(http.Handler) http.Handler
}

func (s *statefulMiddleware) update(nextName string) error {
	next, ok := s.middlewares[nextName]
	if !ok {
		return errors.New(invalidName)
	}

	s.current = next

	return nil
}

func (s *statefulMiddleware) main(next http.Handler) http.Handler {
	return s.current(next)
}

func newStatefulMiddleware(first string, middlewares map[string]func(http.Handler) http.Handler) (*statefulMiddleware, error) {
	current, ok := middlewares[first]
	if !ok {
		return nil, errors.New(invalidName)
	}

	stateful := statefulMiddleware{current: current, middlewares: middlewares}
	return &stateful, nil
}
```

The function `update` exchanges the middlewares based on their name.

The function `main` will be used by mux as a normal middleware.

```go
r.Use(s.middleware.main)
```

The function `newStatefulMiddleware` creates the middleware with a map of middlewares with their names.

```go
	stateful, _ := newStatefulMiddleware(
		"no_verbose",
		map[string]func(http.Handler) http.Handler{
			"no_verbose": noVerboseMiddleware,
			"verbose":    loggingMiddleware})
``` 


Finally, a new handler in the server to choose which middleware I prefer. It will change the middleware base on a JSON message.

```go
func (s *service) config(w http.ResponseWriter, r *http.Request) {
	options := map[string]string{}

	err := json.NewDecoder(r.Body).Decode(&options)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	option, ok := options["option"]
	if !ok {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	err = s.middleware.update(option)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	json.NewEncoder(w).Encode(map[string]bool{"changed": true})
}
```


With all that code, I can exchange the middleware in real-time.

First a request

```
$ curl 0.0.0.0:8080?bar=1
```

To get a verbose log, it was generated by `loggingMiddleware`. 

```
2021/03/19 20:49:41 /?bar=1
2021/03/19 20:49:41 /?bar=1 12.016µs
```

Changing the middleware

```
$ curl -d '{"option": "no_verbose"}' 0.0.0.0:8080/config
```

A new request

```
$ curl 0.0.0.0:8080?bar=1
```

To get a less verbose log, it was generated by `noVerboseMiddleware`.

```
2021/03/19 20:51:43 /?bar=1
```

[All the code put together](https://github.com/juanpabloaj/stateful_middleware_example/blob/0cc32bac377c6e6fefcb3e20d5b450fe7d357231/main.go)

```go
package main

import (
	"encoding/json"
	"errors"
	"log"
	"net/http"
	"time"

	"github.com/gorilla/mux"
)

const invalidName = "invalid middleware name"

type statefulMiddleware struct {
	current     func(http.Handler) http.Handler
	middlewares map[string]func(http.Handler) http.Handler
}

func (s *statefulMiddleware) update(nextName string) error {
	next, ok := s.middlewares[nextName]
	if !ok {
		return errors.New(invalidName)
	}

	s.current = next

	return nil
}

func (s *statefulMiddleware) main(next http.Handler) http.Handler {
	return s.current(next)
}

func newStatefulMiddleware(first string, middlewares map[string]func(http.Handler) http.Handler) (*statefulMiddleware, error) {
	current, ok := middlewares[first]
	if !ok {
		return nil, errors.New(invalidName)
	}

	stateful := statefulMiddleware{current: current, middlewares: middlewares}
	return &stateful, nil
}

func noVerboseMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		log.Println(r.RequestURI)
		next.ServeHTTP(w, r)
	})
}

// loggingMiddleware example from https://github.com/gorilla/mux#examples
func loggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		log.Println(r.RequestURI)

		defer func(startedAt time.Time) {
			log.Println(r.RequestURI, time.Since(startedAt))
		}(time.Now())

		next.ServeHTTP(w, r)
	})
}

type service struct {
	middleware *statefulMiddleware
}

func (s *service) home(w http.ResponseWriter, r *http.Request) {
	json.NewEncoder(w).Encode(map[string]bool{"ok": true})
}

func (s *service) config(w http.ResponseWriter, r *http.Request) {
	options := map[string]string{}

	err := json.NewDecoder(r.Body).Decode(&options)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	option, ok := options["option"]
	if !ok {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	err = s.middleware.update(option)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	json.NewEncoder(w).Encode(map[string]bool{"changed": true})
}

func main() {
	stateful, _ := newStatefulMiddleware(
		"no_verbose",
		map[string]func(http.Handler) http.Handler{
			"no_verbose": noVerboseMiddleware,
			"verbose":    loggingMiddleware})

	s := service{stateful}

	r := mux.NewRouter()
	r.HandleFunc("/config", s.config)
	r.HandleFunc("/", s.home)
	r.Use(s.middleware.main)

	log.Fatal(http.ListenAndServe(":8080", r))
}
```

The source is available in this repository https://github.com/juanpabloaj/stateful_middleware_example 


Some disclaimers:
* I didn't add some things like a mutex to protect the map, because I tried to reduce the number of lines of the example.
* It example could be made with different levels of the log but it was the simplest example that I could create to explain the idea.


What do you think of this approach?

Do you have another way to change the behavior of middleware?

Any suggestions or commentaries are welcome.