23
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 themain
function below.
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.
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
23