Editing Request In Middleware Golang
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.