I want to build a http reverse proxy which checks the HTTP body and send HTTP requests to it's upstream servers after that. How can you do that in go?
Initial attempt (follows) fails because ReverseProxy copies the incoming request, modifies it and sends but the body is already read.
func main() {
backendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
b, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, fmt.Sprintf("ioutil.ReadAll: %s", err), 500)
return
}
// expecting to see hoge=fuga
fmt.Fprintf(w, "this call was relayed by the reverse proxy, body: %s", string(b))
}))
defer backendServer.Close()
rpURL, err := url.Parse(backendServer.URL)
if err != nil {
log.Fatal(err)
}
proxy := func(u *url.URL) http.Handler {
p := httputil.NewSingleHostReverseProxy(u)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, fmt.Sprintf("ParseForm: %s", err), 500)
return
}
p.ServeHTTP(w, r)
})
}(rpURL)
frontendProxy := httptest.NewServer(proxy)
defer frontendProxy.Close()
resp, err := http.Post(frontendProxy.URL, "application/x-www-form-urlencoded", bytes.NewBufferString("hoge=fuga"))
if err != nil {
log.Fatalf("http.Post: %s", err)
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatalf("ioutil.ReadAll: %s", err)
}
fmt.Printf("%s", b)
}
// shows: "http: proxy error: http: ContentLength=9 with Body length 0"
Then my next attempt would be to read the whole body into bytes.Reader and use that to check the body content, and Seek to the beginning before sending to upstream servers. But then I have to re-implement ReverseProxy which I would like to avoid. Is there any other elegant way?
edit:
As commented above, the parsed form will be empty in this case. You will need to manually parse the form from the body.
The request.Body
is a io.ReaderCloser
, because this describes the rx
part of a tcp connection. But in your use case you need to read everything since you are parsing the body into a form. The trick here is to reassign the r.Body
with a io.ReaderCloser
object derived from the already read data. Here is what I would do:
// before calling r.ParseForm(), get the body
// as a byte slice
body, err := ioutil.ReadAll(r.Body)
// after calling r.ParseForm(), reassign body
r.Body = ioutil.NopCloser(bytes.NewBuffer(body))
bytes.NewBuffer(body)
converts the body byte slice into a io.Reader
, and ioutil.NopCloser
convertts a io.Reader
into a io.ReaderCloser
with a nop
Close()
method.
package main
import "net/http"
import "net/http/httputil"
import "net/url"
import "net/http/httptest"
import "fmt"
import "log"
import "bytes"
import "io/ioutil"
func main() {
backendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
b, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, fmt.Sprintf("ioutil.ReadAll: %s", err), 500)
return
}
// expecting to see hoge=fuga
fmt.Fprintf(w, "this call was relayed by the reverse proxy, body: %s", string(b))
}))
defer backendServer.Close()
rpURL, err := url.Parse(backendServer.URL)
if err != nil {
log.Fatal(err)
}
proxy := func(u *url.URL) http.Handler {
p := httputil.NewSingleHostReverseProxy(u)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// read out body into a slice
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, fmt.Sprintf("Error reading body: %s", err), 500)
return
}
// inspect current body here
if err := r.ParseForm(); err != nil {
http.Error(w, fmt.Sprintf("ParseForm: %s", err), 500)
return
}
// assign a new body with previous byte slice
r.Body = ioutil.NopCloser(bytes.NewBuffer(body))
p.ServeHTTP(w, r)
})
}(rpURL)
frontendProxy := httptest.NewServer(proxy)
defer frontendProxy.Close()
resp, err := http.Post(
frontendProxy.URL,
"application/x-www-form-urlencoded",
bytes.NewBufferString("hoge=fuga"))
if err != nil {
log.Fatalf("http.Post: %s", err)
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatalf("ioutil.ReadAll: %s", err)
}
fmt.Printf("%s", b)
}
You can set Director
handler to httputil.ReverseProxy
Document: https://golang.org/pkg/net/http/httputil/#ReverseProxy
Here's an example code which reads content body from request and proxies from localhost:8080
to localhost:3333
package main
import (
"bytes"
"io/ioutil"
"log"
"net/http"
"net/http/httputil"
)
func main() {
director := func(req *http.Request) {
if req.Body != nil {
// read all bytes from content body and create new stream using it.
bodyBytes, _ := ioutil.ReadAll(req.Body)
req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
// create new request for parsing the body
req2, _ := http.NewRequest(req.Method, req.URL.String(), bytes.NewReader(bodyBytes))
req2.Header = req.Header
req2.ParseForm()
log.Println(req2.Form)
}
req.URL.Host = "localhost:3333"
req.URL.Scheme = "http"
}
proxy := &httputil.ReverseProxy{Director: director}
log.Fatalln(http.ListenAndServe(":8080", proxy))
}