How to write an example tutorial for Go middleware

  • 2020-06-12 09:29:39
  • OfStack

The introduction

In the context of web development, "middleware" usually means "part of an application that wraps the original application and adds some additional functionality." The concept always seems to go ununderstood, but I think middleware is great.

First, a good piece of middleware has a responsibility to be pluggable and self-sufficient. This means that you can embed your middleware at the interface level and it will run directly. It doesn't affect how you code, it's not a framework, it's just a layer in your request processing. There is no need to rewrite your code, if you want to use one of the middleware features, you can insert it there, if you don't want to use it, you can just remove it.

Throughout the Go language, middleware is very common, even in the standard library. Although it may not be obvious at first, the function StripText or TimeoutHandler in the standard library net/http is what the middleware looks like when we define and, handle requests and accordingly they wrap your handler and handle 1 extra step.

At first, we thought that writing middleware seemed easy, but when we actually wrote it, we ran into all sorts of pits. Let's look at some examples.

1. Read the request

In our example, all middleware will accept http. The handler takes 1 argument and returns 1 http.Handler. This makes it easy for people to string together intermediate products. The basic model for all of our intermediate products is this:


func X(h http.Handler) http.Handler {
 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 // Something here...
 h.ServeHTTP(w, r)
 })
}

We want to redirect all requests to one slash -- say /message/ -- to their non-slash equivalent, such as /message. We could write it like this:


func TrailingSlashRedirect(h http.Handler) http.Handler {
 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 if r.URL.Path != "/" && r.URL.Path[len(r.URL.Path)-1] == '/' {
 http.Redirect(w, r, r.URL.Path[:len(r.URL.Path)-1], http.StatusMovedPermanently)
 return
 }
 h.ServeHTTP(w, r)
 })
}

Is it easy?

2. Request for modification

Let's say we want to add a title to the request or modify it. http. Handler document indicates:

[

The handler should not modify the provided request except to read the body.

]

Go standard library copies ES50en.ES51en. We should do the same for the request object before passing it to the response chain. Suppose we want to set the Request-ES53en header on each request for internal tracking. Create a shallow copy of *Request and modify the title before the agent.


func RequestID(h http.Handler) http.Handler {
 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 r2 := new(http.Request)
 *r2 = *r
 r2.Header.Set("X-Request-Id", uuid.NewV4().String())
 h.ServeHTTP(w, r2)
 })
}

3. Write the response header

If you want to set the response headers, you can just write them and then proxy the request.


func Server(h http.Handler, servername string) http.Handler {
 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 w.Header().Set("Server", servername)
 h.ServeHTTP(w, r)
 })
}

The problem with this is that if the internal processor also sets the server header, your header will be overwritten. This can cause problems if you don't want to expose the server headers of your internal software, or if you want to remove the headers before sending the response to the client.

To do this, we must implement the ResponseWriter interface ourselves. Most of the time, we will only proxy to the underlying ResponseWriter, but if the user tries to write a response, we will sneak in and add our title.


type serverWriter struct {
 w http.ResponseWriter
 name string
 wroteHeaders bool
}

func (s *serverWriter) Header() http.Header {
 return s.w.Header()
}

func (s *serverWriter) WriteHeader(code int) http.Header {
 if s.wroteHeader == false {
 s.w.Header().Set("Server", s.name)
 s.wroteHeader = true
 }
 s.w.WriteHeader(code)
}

func (s *serverWriter) Write(b []byte) (int, error) {
 if s.wroteHeader == false {
 // We hit this case if user never calls WriteHeader (default 200)
 s.w.Header().Set("Server", s.name)
 s.wroteHeader = true
 } return s.w.Write(b)
}

To use it in our middleware, we would write:


func Server(h http.Handler, servername string) http.Handler {
 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 sw := &serverWriter{
 w: w,
 name: servername,
 }
 h.ServeHTTP(sw, r)
 })
}

The problem

What if the user never calls Write or WriteHeader? For example, there is a 200 state and it is empty body, or a response to an option request -- none of our intercepting functions will run. Therefore, we should add validation after the ServeHTTP call.


func Server(h http.Handler, servername string) http.Handler {
 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 sw := &serverWriter{
 w: w,
 name: servername,
 }
 h.ServeHTTP(sw, r)
 if sw.wroteHeaders == false {
 s.w.Header().Set("Server", s.name)
 s.wroteHeader = true
 }
 })
}

Other ResponseWriter interfaces

The ResponseWriter interface requires only three methods. But in practice, it can also respond to other interfaces, such as ES93en.Pusher. Your middleware may accidentally disable HTTP/2 support, which is not good.


// Push implements the http.Pusher interface.
func (s *serverWriter) Push(target string, opts *http.PushOptions) error {
 if pusher, ok := s.w.(http.Pusher); ok {
 return pusher.Push(target, opts)
 }
 return http.ErrNotSupported
}

// Flush implements the http.Flusher interface.
func (s *serverWriter) Flush() {
 f, ok := s.w.(http.Flusher)
 if ok {
 f.Flush()
 }
}

conclusion

Through the above learning, I wonder if you have a complete understanding of Go middleware. You can also try Go to write a middleware.


Related articles: