Changing the middleware behavior with mux

juanpabloaj

JuanPablo

Posted on March 20, 2021

Changing the middleware behavior with mux

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

Probably you have used 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.

Here a middleware example based on the documentation.

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))
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
2021/03/19 19:55:17 /?bar=1
2021/03/19 19:55:17 /?bar=1 17.395µs
Enter fullscreen mode Exit fullscreen mode

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.

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)
    })
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
2021/03/19 20:39:12 /?bar=1
Enter fullscreen mode Exit fullscreen mode

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.

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
}
Enter fullscreen mode Exit fullscreen mode

The function update exchanges the middlewares based on their name.

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

r.Use(s.middleware.main)
Enter fullscreen mode Exit fullscreen mode

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

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

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

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})
}
Enter fullscreen mode Exit fullscreen mode

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

First a request

$ curl 0.0.0.0:8080?bar=1
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Changing the middleware

$ curl -d '{"option": "no_verbose"}' 0.0.0.0:8080/config
Enter fullscreen mode Exit fullscreen mode

A new request

$ curl 0.0.0.0:8080?bar=1
Enter fullscreen mode Exit fullscreen mode

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

2021/03/19 20:51:43 /?bar=1
Enter fullscreen mode Exit fullscreen mode

All the code put together

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))
}
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
juanpabloaj
JuanPablo

Posted on March 20, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related