Multiple middleware and chaining

In the previous section, we built a single middleware to perform an action before or after the 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 the preceding section. Let us create a city API for saving city details. For simplicity's sake, the API will have one POST method, and the body consists of two fields: city name and city area.

Let us think about a scenario where an API developer only allows the JSON media type from clients and also needs to send the server time in UTC back to the client for every request. Using middleware, we can do that.

The functions of two middleware are:

  • 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

First, let us create the POST API:

package main

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

type city struct {
Name string
Area uint64
}

func mainLogic(w http.ResponseWriter, r *http.Request) {
// Check if method is POST
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()
// Your resource creation logic goes here. For now it is plain print to console
fmt.Printf("Got %s city with area of %d sq miles! ", tempCity.Name, tempCity.Area)
// Tell everything is fine
w.WriteHeader(http.StatusOK)
w.Write([]byte("201 - Created"))
} else {
// Say method not allowed
w.WriteHeader(http.StatusMethodNotAllowed)
w.Write([]byte("405 - Method Not Allowed"))
}
}

func main() {
http.HandleFunc("/city", mainLogic)
http.ListenAndServe(":8000", nil)
}

If we run this:

go run cityAPI.go

Then give a CURL request:

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}'

Go gives us the following:

Got New York city with area of 304 sq miles!
Got Boston city with area of 89 sq miles!

CURL responses will be:

201 - Created
201 - Created

In order to chain, we need to pass the handler between multiple middlewares.

Here is the program in simple steps:

  • We created a REST API with a POST as the allowed method. It is not complete because we are not storing data to a database or file.
  • We imported the json package and used it to decode the POST body given by the client. Next, we created a structure that maps the JSON body.
  • Then, JSON got decoded and printed the information to the console.

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. The complete code looks like this:

package main
import (
"encoding/json"
"log"
"net/http"
"strconv"
"time"
)
type city struct {
Name string
Area uint64
}
// Middleware to check content type as JSON
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)
})
}
// Middleware to add server timestamp for response cookie
func setServerTimeCookie(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
handler.ServeHTTP(w, r)
// Setting cookie to each and every 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")
})
}
func mainLogic(w http.ResponseWriter, r *http.Request) {
// Check if method is POST
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()
// Your resource creation logic goes here. For now it is plain print to console
log.Printf("Got %s city with area of %d sq miles! ", tempCity.Name, tempCity.Area)
// Tell everything is fine
w.WriteHeader(http.StatusOK)
w.Write([]byte("201 - Created"))
} else {
// Say method not allowed
w.WriteHeader(http.StatusMethodNotAllowed)
w.Write([]byte("405 - Method Not Allowed"))
}
}
func main() {
mainLogicHandler := http.HandlerFunc(mainLogic)
http.Handle("/city", filterContentType(setServerTimeCookie(mainLogicHandler)))
http.ListenAndServe(":8000", nil)
}

Now, if we run this:

go run multipleMiddleware.go

And run this for the CURL command:

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

The output is:

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

But if we try to remove Content-Type:application/json from the CURL command, the middleware blocks us from executing the main handler:

curl -i -X POST http://localhost:8000/city -d '{"name":"New York", "area":304}' 
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

And the cookie will be set from the other middleware.

In the preceding program, we used log instead of the fmt package. Even though both do the same thing, log formats the output by attaching a timestamp of the log. It can also be easily directed to a file. 

There are a few interesting things in this program. The middleware functions we defined have quite common use cases. We can extend them to perform any action. The program is composed of many elements. If you read it function by function, the logic can be easily unwound. Take a look at the following points:

  • A struct called city was created to store city details, as in the last example.
  • filterContentType is the first middleware we added. It actually checks the content type of the request and allows or blocks the request from proceeding further. For checking, we are using r.Header.GET (content type). If it is application/json, we are allowing the request to call the handler.ServeHTTP function, which executes the mainLogicHandler code.
  • setServerTimeCookie is the second middleware that we designed to add a cookie to the response with a value of the server time. We are using Go's time package to find the current UTC time in the Unix epoch.
  • For the cookie, we are setting Name and Value. The cookie also accepts another parameter called Expire, which tells the expiry time of the cookie.
  • If the content type is not application/json, our application returns the 415-Media type not supported status code.
  • In the mainhandler, we are using json.NewDecoder to parse the JSON and fill them into the city struct.
  • strconv.FormatInt allows us to convert an int64 number to a string. If it is a normal int, then we use strconv.Itoa.
  • 201 is the correct status code to be returned when the operation is successful. For all other methods, we are returning 405, that is, a method not allowed.

The form of chaining we did here is readable for two to three middleware:

http.Handle("/city", filterContentType(setServerTimeCookie(mainLogicHandler)))

If an API server wishes a request to go through many middlewares, 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 chapter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.178.151