Reaves.dev

v0.1.0

built using

Phoenix v1.7.12

Editing Request In Middleware Golang

Stephen M. Reaves

::

2022-08-22

Example of how to read/edit http requests in middleware.

Problem

Assuming you are developing in golang, using gorilla/mux, you may be familiar with their idea of middleware. If not, we can take a look at the function signature and learn a lot about what it’s trying to do.

// MiddlewareFunc is a function which receives an http.Handler and returns
// another http.Handler. Typically, the returned handler is a closure which
// does something with the http.ResponseWriter and http.Request passed to it,
// and then calls the handler passed as parameter to the MiddlewareFunc.
type MiddlewareFunc func(http.Handler) http.Handler

For once,the documentation is quite helpful. Basically it’s a way to chain functions in front of an http handler and make transformations on either the request or the response writer. This is extremely powerful, but it comes with a cost. Reading from the http request causes it to go blank. So how do we solve this?

Lets’ take a look at the following example:

func run() error {
	router := mux.NewRouter()

	router.HandleFunc(/, func(rw http.ResponseWriter, r *http.Request) {
		log.Println("In Handler")

		body, err := ioutil.ReadAll(r.Body)
		if err != nil {
			log.Fatalln(err)
		}

		log.Println(Body, string(body))

		ctx := r.Context()
		log.Println(ctx.Value(foo))
	})

	router.Use(func(next http.Handler) http.Handler {
        // Middleware func
	})

	server := http.Server{
		Addr:    ADDR,
		Handler: router,
	}

    // Test client

	return server.ListenAndServe()
}

Solution

We have a simple web server with a single function listening on the / path. That’s not that interesting. What is interesting is the router.Use() portion. This is where we define our middleware function. We can use something like this:

	router.Use(func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			log.Println("In MiddleWare")

			body, err := ioutil.ReadAll(r.Body)
			if err != nil {
				log.Fatalln(err)
			}

			body2, err2 := ioutil.ReadAll(r.Body)
			if err2 != nil {
				log.Fatalln(err)
			}

			// Set a new body which will simulate the data we read.
			// Taken from https://stackoverflow.com/questions/43021058/golang-read-request-body-multiple-times
			r.Body = ioutil.NopCloser(bytes.NewBuffer(body))

			log.Println(Body 1, string(body))
			log.Println(Body 2, string(body2))

			ctx := r.Context()
			ctx = context.WithValue(ctx, foo, bar)

			*r = *r.WithContext(ctx)

			next.ServeHTTP(w, r)
		})
	})

The real meat and potatoes is the line about ioutil.NopCloser. Here, we are creating a buffer based on what we previously read from r.Body. Then we wrap in a NopCloser which allows us to satisfy the Closer interface closing. Finally, we store that back as the r.Body so the handler function can re-read it. We also have some added business about how to add values to the context. Similarly we will have to save the values back into r so the handler can use them.

We can run this by creating a little test client.

for {
    time.Sleep(5 * time.Second)

    fmt.Println(Sending Request)
    buf := bytes.NewBufferString(foo)
    if _, err := http.Post(fmt.Sprintf("http://localhost%s/", ADDR),
            http.DetectContentType(buf.Bytes()),
            buf); err != nil {
        log.Println(err)
    }
}

All this client does is send the word foo to the server every 5 seconds. We can put everything all together and get the following:

main.go
/**
 * File: main.go
 * Written by:  Stephen M. Reaves
 * Created on:  Mon, 22 Aug 2022
 * Description: Demo showing how to edit request in middleware.
 */

package main

import (
	bytes
	context
	fmt
	io/ioutil
	log
	net/http
	time

	github.com/gorilla/mux
)

func main() {
	// Optionally parse flags here

	if err := run(); err != nil {
		log.Fatalln(err)
	}
}

const (
	ADDR string = :8080
)

func run() error {
	router := mux.NewRouter()

	router.HandleFunc(/, func(rw http.ResponseWriter, r *http.Request) {
		log.Println(In Handler)

		body, err := ioutil.ReadAll(r.Body)
		if err != nil {
			log.Fatalln(err)
		}

		body2, err2 := ioutil.ReadAll(r.Body)
		if err2 != nil {
			log.Fatalln(err)
		}

		log.Println(Body, string(body))
		log.Println(Body2, string(body2))

		ctx := r.Context()
		log.Println(ctx.Value(foo))
	})

	router.Use(func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			log.Println(In MiddleWare)

			body, err := ioutil.ReadAll(r.Body)
			if err != nil {
				log.Fatalln(err)
			}

			body2, err2 := ioutil.ReadAll(r.Body)
			if err2 != nil {
				log.Fatalln(err)
			}

			// Set a new body which will simulate the data we read.
			// Taken from https://stackoverflow.com/questions/43021058/golang-read-request-body-multiple-times
			r.Body = ioutil.NopCloser(bytes.NewBuffer(body))
			log.Println(Body 1, string(body))
			log.Println(Body 2, string(body2))

			ctx := r.Context()
			ctx = context.WithValue(ctx, foo, bar)

			*r = *r.WithContext(ctx)

			next.ServeHTTP(w, r)
		})
	})

	server := http.Server{
		Addr:    ADDR,
		Handler: router,
	}

	go func() {
		for {
			time.Sleep(5 * time.Second)

			fmt.Println(Sending Request)
			buf := bytes.NewBufferString(foo)
			if _, err := http.Post(fmt.Sprintf("http://localhost%s/", ADDR), http.DetectContentType(buf.Bytes()), buf); err != nil {
				log.Println(err)
			}
		}
	}()

	fmt.Println(Listening on port, ADDR)
	return server.ListenAndServe()
}

You can run this code in the terminal and it will run the server and client all in one, while logging to the terminal. I also encorouage you to comment out the NopCloser line and see how the output changes.