Read Go HTTP Request body multiple times

Or any other io.Reader in a reusable way

I think we can all agree that writing HTTP servers in Go is an overall simple and pleasant affair. But what happens when we find ourselves needing to read the body of an incoming request multiple times? A common use case would be a middleware that needs to read and verify the request body before it can be processed further, or another that needs to log the request body while still allowing the body to be read later on.

The Problem

After reading once from the io.Reader implemented by the body field of Go http requests, it becomes impossible to read from it again. There is also no builtin way to simply "reset" the body as part of the interface.

The simplest solution

For the simplest solution, we simply need to make sure that every time we read through the request body, we read through it fully and then replace it with a new reader. Simple.

body, err := io.ReadAll(r.Body)
// Replace the body with a new reader after reading from the original
r.Body = io.NopCloser(bytes.NewBuffer(body))

We can even make use of an io.TeeReader and bytes.Buffer.

buf := bytes.Buffer{} // A Buffer is both a Reader and Writer
req.Body = io.TeeReader(req.Body, &buf)
// Do some body reading, then replace the body with the buffer
req.Body = &buf

Simple Reusable Reader implementation

Because Go's interfaces work via structural typing, we can very easily implement the io.Reader interface with a custom type as a drop-in replacement. Thus creating an io.Reader that can be "read from" an infinite number of times.

type reusableReader struct {
    io.Reader
    readBuf *bytes.Buffer
    backBuf *bytes.Buffer
}

func ReusableReader(r io.Reader) io.Reader {
    readBuf := bytes.Buffer{}
    readBuf.ReadFrom(r) // error handling ignored for brevity
    backBuf := bytes.Buffer{}

    return reusableReader{
        io.TeeReader(&readBuf, &backBuf),
        &readBuf,
        &backBuf,
    }
}

func (r reusableReader) Read(p []byte) (int, error) {
    n, err := r.Reader.Read(p)
    if err == io.EOF {
        r.reset()
    }
    return n, err
}

func (r reusableReader) reset() {
    io.Copy(r.readBuf, r.backBuf) // nolint: errcheck
}

Basic usage

A simple example of using our reusable reader with basically any io.Reader implementation.

func main() {
    text := "Lorem ipsum dolor sit amet"
    r := ReusableReader(strings.NewReader(text))

    readAndPrint(r)
    readAndPrint(r)
    readAndPrint(r)
}

func readAndPrint(r io.Reader) {
    b, _ := io.ReadAll(r)
    fmt.Printf("%s\n", string(b))
}

// "Lorem ipsum dolor sit amet"
// "Lorem ipsum dolor sit amet"
// "Lorem ipsum dolor sit amet"

Worth noting that if only dealing with strings.Readers or bytes.Readers, it is infinitely easier to simply seek back to the beginning of those readers since they implement the io.Seeker interface

r := strings.NewReader(text)
r.Seek(0, 0) // will effectively reset the reader back to the beginning

Request body usage

Things get more complicated when dealing with http.Request body fields, which don't implement the io.Seeker interface. In that case, for any http.Request we can easily use our reusable reader in place of its Body.

func handler(w http.ResponseWriter, r *http.Request) {
    r.Body = io.NopCloser(ReusableReader(r.Body))
    // Perform any reads however much we like from here on out
}

We needed to wrap our ReusableReader with an io.NopCloser in order to adhere to the io.ReadCloser interface. We could realistically simplify this case by implementing the io.Closer interface, just like the io.NopCloser does.

func (r ReusableReader) Close() error { return nil }

Which would allow the above example to be a tad simpler.

r.Body = ReusableReader(r.Body)

A more real-world example

Infinitely re-reading a static byte reader isn't all that exciting. So let's assume that we are building a web application and have created the following middleware for our request handlers, which both require the reading of the request body.

func logRequest(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        msg := fmt.Sprintf("%s %s", r.Method, r.URL.Path)

        if body, _ := io.ReadAll(r.Body); len(body) > 0 {
            msg += fmt.Sprintf(" Body: %s", string(body))
        }

        log.Print(msg)
        next.ServeHTTP(w, r)
    })
}

func verifyRequest(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        body, err := io.ReadAll(r.Body)
        // Perform some request verification here
        if err != nil || len(body) == 0 {
            log.Printf("%d: Request verification failed", http.StatusBadRequest)
            w.WriteHeader(http.StatusBadRequest)
            return
        }

        log.Printf("Request verified: %s", string(body))
        next.ServeHTTP(w, r)
    })
}

If used as is, then whichever of these middleware that we would run second would not be able to read the request body, since it would have already been read from by the first. This is where our reusable reader can come in handy, and we could simply wrap it's logic around a third middleware that should be ran first in the chain.

func reuseBody(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        r.Body = io.NopCloser(ReusableReader(r.Body))
        next.ServeHTTP(w, r)
    })
}

Now that we have all middleware in place, we can setup our http server and make use of the infinitely readable http.Request body.

Let's assume we have a handler called greet which will greet the user based on the request body payload - again needing to read from the request body after passing it through both of the other middleware already.

func main() {
    greetHandler := http.HandlerFunc(greet)

    mux := http.NewServeMux()
    mux.Handle("/greet", reuseBody(logRequest(verifyRequest(greetHandler))))

    fmt.Println("Listening on port http://localhost:9000/")
    log.Fatalln(http.ListenAndServe(":9000", mux))
}

Final thoughts and drawbacks

While this is all fine and dandy with the simple examples above, it's important to remember that this reusable reader is by nature not a very efficient implementation of the io.Reader interface; mainly because we require reading all of the data up front into a buffer. Which won't work well for anything large like files or larger streams of data.

Another issue with this approach is that we are assuming that everyone attempting to read from the reader knows and assumes that it is an instance of a ReusableReader. At which point it might be better/safer for middleware to simply read from the reader, process any data and replace the reader with a new one like mentioned before.