Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ReverseProxy depending on the request.Body in golang

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?

like image 270
mash Avatar asked Oct 15 '25 03:10

mash


1 Answers

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))
}
like image 64
ymonad Avatar answered Oct 17 '25 04:10

ymonad