Custom middleware basics with Go. No frameworks.

So you want to add some custom middleware to a Go api? There's many elegant and advanced ways to do this, which is great! - but who wants to simply use a pre-cooked black-box when you can cut your own and have some sort of idea how others are doing it?
Here we're going to demo some fairly trivial hand-rolled middleware functions that will wrap the original function handling our request.
  • Let's create a fairly standard looking function that takes a ResponseWriter and a Request.
  • func postHandler(w http.ResponseWriter, r *http.Request) {
        if r.Method == http.MethodPost {
    
            // create a variable of our defined struct type Lesson
            var tempLesson Lesson
    
            decoder := json.NewDecoder(r.Body)
            err := decoder.Decode(&tempLesson)
            if err != nil {
                panic(err)
            }
    
            defer r.Body.Close()
    
            fmt.Printf("Title: %s. Summary: %s\n", tempLesson.Title, tempLesson.Summary)
            w.WriteHeader(http.StatusCreated)
            w.Write([]byte("201 - Created"))
    
        } else {
            w.WriteHeader(http.StatusMethodNotAllowed)
            w.Write([]byte("405 - Method Not Allowed"))
        }
    }
    Thing to note:
  • we can use the http types instead of r.Method == "POST" for better clarity.
  • In the snippet above you can see we mention the type we create called Lesson, you will see this depicted in the snippet that shows the package, imports and the main function below.
  • Adding Middleware
    So, now the middleware we'd like to apply is firstly to check that we're accepting JSON format, or prevent the request from going anywhere. We can do that like this.
    func filterContentType(handler http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            log.Println("Inside filterContentType middleware, before the request is handled")
    
            // filter requests by mime type
            if r.Header.Get("Content-Type") != "application/json" {
                w.WriteHeader(http.StatusUnsupportedMediaType)
                w.Write([]byte("415 - Unsupported Media Type. JSON type is expected"))
                return
            }
    
            // handle the request
            handler.ServeHTTP(w, r)
    
            log.Println("Inside filterContentType middleware, after the request was handled")
        })
    }
    Things to note:
  • we take an http.Handler and return an http.Handler
  • we can do stuff before and after handling the request. Here we're doing a but of useless printing but you get the idea.
  • The next thing we want to do is apply another middleware and this time we're going to add a cookie attribute with the server timestamp. This magic can be woven like so...
    unc setServerTimeCookie(handler http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            log.Println("Inside setServerTimeCookie middleware, before the request is handled")
    
            // handles the request
            handler.ServeHTTP(w, r)
    
            cookie := http.Cookie{Name: "Server Time(UTC)", Value: strconv.FormatInt(time.Now().Unix(), 10)}
            http.SetCookie(w, &cookie)
    
            log.Println("Inside setServerTimeCookie middleware, after the request is handled")
        })
    }
    Things to note:
  • again you can see we can have a before and after routine demonstrating we have standard wrapping capabilities yet again.
  • we create and set the cookie value using an http in-built method.
  • Testing our work
    OK, so no we need a way to run this so we can flesh out our main method like this.
    package main
    
    import (
        "encoding/json"
        "fmt"
        "log"
        "net/http"
        "strconv"
        "time"
    )
    
    type Lesson struct {
        Title   string
        Summary string
    }
    
    func main() {
        port := ":8080"
    
        originalHandler := http.HandlerFunc(postHandler)
        http.Handle("/lesson", filterContentType(setServerTimeCookie(originalHandler)))
    
        //http.HandleFunc("/city", poastHandler)
        log.Fatal(http.ListenAndServe(port, nil))
    }
    things to note here:
  • the originalHandler is an http.HandlerFunc type.
  • you can see that each middleware effectively wraps what it receives, this allows us to chain the middlware functions around the original handler meaning we can have a a line like filterContentType(setServerTimeCookie(originalHandler))
  • we could even do it on a one liner, but clarity starts to dwindle. If you want to try it swap to http.Handle("/lesson", filterContentType(setServerTimeCookie(http.HandlerFunc(postHandler))))
  • so, now we're going to need some positive and negative tests. To do so easily we can manually test on the CLI using curl.
    So, let's see if we can process non-json input as the request.
    curl http://localhost:8080/city -d '{"title": "middleware with Go", "summary": "learn some middleware concepts with a quick and dirty demo"}'
    like me, you should be met with a 415 - Unsupported Media Type. JSON type expected% response to your test.
    Now let's try what should hopefully be a successful attempt.
    curl -i -H "Content-Type: application/json" -X POST http://localhost:8080/city -d '{"title": "Middleware demo with Go", "summary": "Hopefully a successful demo now that we have specified mime type and the method"}'
    
    Hopefully your output is similar to below. 
    
    `HTTP/1.1 200 OK
    Date: Tue, 27 Jul 2021 20:09:52 GMT
    Content-Length: 13
    Content-Type: text/plain; charset=utf-8
    
    201 - Created%`
    That wraps up the post. I hope you found this quick overview to be of some use if you too are messing about with Go and looking to grasp what middleware is and how it works.
    Best wishes

    42

    This website collects cookies to deliver better user experience

    Custom middleware basics with Go. No frameworks.