Hands-On RESTful Web Services with Go
上QQ阅读APP看书,第一时间看更新

Multiple middleware and chaining

In the previous section, we built a single middleware to perform an action before or after a request hits the handler. It is also possible to chain a group of middleware. In order to do that, we should follow the same closure logic as in the preceding section. Let's create a cityAPI program for saving city details. For simplicity's sake, the API will have one POST method, and the body will consist of two fields: city name and city area.

Let's us think about a scenario where a client is only allowed to send a JSON Content-Type request to an API. The main function of the API is to send a response to the client with a UTC timestamp cookie attached to it. We can add that content check in the middleware.

The functions of the two middleware are as follows:

  • In the first middleware, check whether the content type is JSON. If not, don't allow the request to proceed.
  • In the second middleware, add a timestamp called Server-Time (UTC) to the response cookie.

Before adding the middleware, Let's create a POST API that collects the name and area of a city and returns a message with a status code of 201, to show it has been successfully created. This can be done in the following way:

  1. Create a file for our program, like this:
touch -p $GOPATH/src/github.com/git-user/chapter3/cityAPI/main.go
  1. Now, write the function that handles the POST request from the client. It decodes the body and reads the name and area, and fills them into a struct called city, like this:
type city struct {
Name string
Area uint64
}

func postHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
var tempCity city
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&tempCity)
if err != nil {
panic(err)
}
defer r.Body.Close()
fmt.Printf("Got %s city with area of %d sq miles!\n",
tempCity.Name, tempCity.Area)
w.WriteHeader(http.StatusOK)
w.Write([]byte("201 - Created"))
} else {
w.WriteHeader(http.StatusMethodNotAllowed)
w.Write([]byte("405 - Method Not Allowed"))
}
}

postHandler is handling a client request in this snippet. It returns a status code of 405 - Method Not Allowed if a client tries to perform a GET request. json.NewDecoder is used to read the body from a request. Decode maps the body parameters to a struct, of the city  type.

  1. Now comes the main logic, as shown here:
package main

import (
"encoding/json"
"fmt"
"net/http"
)

func main() {
http.HandleFunc("/city", postHandler)
http.ListenAndServe(":8000", nil)
}
  1. We can start the API server by using the following code:
go run $GOPATH/src/github.com/git-user/chapter3/cityAPI/main.go
  1. Then, fire a couple of curl requests, like so:
curl -H "Content-Type: application/json" -X POST http://localhost:8000/city -d '{"name":"New York", "area":304}'

curl -H "Content-Type: application/json" -X POST http://localhost:8000/city -d '{"name":"Boston", "area":89}'
  1. The server logs this output:
Got New York city with area of 304 sq miles!
Got Boston city with area of 89 sq miles!

The curl responses are as follows:

201 - Created
201 - Created
  1. Now comes the content checks. In order to chain middleware functions, we have to pass the handler between multiple middleware. Only one handler is involved in the preceding example. But now, for the upcoming task, the idea is to pass the main handler to multiple middleware handlers. We can modify the cityAPI program to a new file, like this:
touch -p $GOPATH/src/github.com/git-user/chapter3/multipleMiddleware/main.go
  1. Let's first create the content-check middleware. Let's call it filterContentType. This middleware checks the MIME header from the request and, if it is not JSON, returns a response of status code 415- Unsupported Media Type, as shown in the following code block:
func filterContentType(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter,
r *http.Request) {
log.Println("Currently in the check content type middleware")
// Filtering requests by MIME type
if r.Header.Get("Content-type") != "application/json" {
w.WriteHeader(http.StatusUnsupportedMediaType)
w.Write([]byte("415 - Unsupported Media Type. Please send JSON"))
return
}
handler.ServeHTTP(w, r)
})
}
  1. Now, Let's define a second middleware called setServerTimeCookie. After receiving a proper content type while sending a response back to the client, this middleware adds a cookie called Server-Time(UTC) with the server UTC timestamp as the value, as shown in the following code block:
func setServerTimeCookie(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter,
r *http.Request) {
handler.ServeHTTP(w, r)
// Setting cookie to every API response
cookie := http.Cookie{Name: "Server-Time(UTC)",
Value: strconv.FormatInt(time.Now().Unix(), 10)}
http.SetCookie(w, &cookie)
log.Println("Currently in the set server time middleware")
})
}
  1. The main function has a slight variation in the mapping of a route to the handler. It uses nested function calls for chaining middleware, as can be seen here:
func main() {
originalHandler := http.HandlerFunc(handle)
http.Handle("/city",
filterContentType(setServerTimeCookie(originalHandler)))
http.ListenAndServe(":8000", nil)
}

We chain the middleware by using filterContentType(setServerTimeCookie(originalHandler)). Please carefully observe the order of chaining.

  1. Now, run the updated server, as follows:
go run $GOPATH/src/github.com/git-user/chapter3/multipleMiddleware/main.go

Then, fire a curl request, like this:

curl -i -H "Content-Type: application/json" -X POST http://localhost:8000/city -d '{"name":"Boston", "area":89}'

The response output is the following:

HTTP/1.1 200 OK
Date: Sat, 27 May 2017 14:35:46 GMT
Content-Length: 13
Content-Type: text/plain; charset=utf-8

201 - Created
  1. But if we remove Content-Type: application/json from the curl request, the middleware blocks us from executing the main handler, as shown in the following code block:
curl -i -X POST http://localhost:8000/city -d '{"name":"New York", "area":304}'

Result:

HTTP/1.1 415 Unsupported Media Type
Date: Sat, 27 May 2017 15:36:58 GMT
Content-Length: 46
Content-Type: text/plain; charset=utf-8

415 - Unsupported Media Type. Please send JSON

This is the simplest way of chaining middleware in Go API servers.

If an API server wishes a request to go through many middleware, then how can we make that chaining simple and readable? There is a very good library called alice to solve this problem. It allows you to semantically order and attach your middleware to the main handler. We will see it briefly in the next section.