chris
chris

Reputation: 4351

mux.Vars is empty when using httputil.ReverseProxy

I am trying to use gorilla mux and httputil.ReverseProxy together, but when trying to get the mux.Vars it is empty. According to https://golang.org/src/net/http/httputil/reverseproxy.go?s=2744:2819#L93 it seems like the http.Request pointer is a shallow copy of the original request, which should still work.

Any ideas?

https://play.golang.org/p/JpjNvEMIFB

package main

import (
    "github.com/gorilla/mux"
    "log"
    "net/http"
    "net/http/httputil"
    "net/url"
)

type route struct {
    match string
    base  string
}

var routes = []route{
    // proxy http://localhost:3000/api/foo/bar => https://api.bar.com/5/foo/bar
    route{match: "/api/{path}", base: "https://api.bar.com/5"},
    route{match: "/sales/{path}", base: "https://sales.bar.com/3"},
}

func NewProxy(r *route) http.Handler {
    director := func(req *http.Request) {
        out, _ := url.Parse(r.base)

        req.URL.Scheme = out.Scheme
        req.URL.Host = out.Host
        req.URL.Path = out.Path + "/" + mux.Vars(req)["path"] // mux Vars are empty here
    }
    return &httputil.ReverseProxy{Director: director}
}

func main() {
    for _, route := range routes {
        http.Handle(route.match, NewProxy(&route))
    }

    log.Println("Listening on port 8080")
    http.ListenAndServe(":8080", nil)
}

Upvotes: 3

Views: 1012

Answers (1)

Alvivi
Alvivi

Reputation: 3331

You have two different problems here.

The first one, you are not using a mux.Router, so gorilla/mux has not the opportunity to pre-process your request. In other words, the requests are going directly from http package to your reverse proxies. This issue has an easy fix:

r := mux.NewRouter()
for _, route := range routes {
    r.Handle(route.match, NewProxy(&route))
}
http.Handle("/", r)

The second problem is more tricky than the first one. This issue is related to how is mux package implemented. If you look mux.Vars() implementation, you will see that it uses something called Context. A Context, as described in the official documentation, is something that stores values shared during a request lifetime. A simplified Context implementation will be:

type Context map[*http.Request]interface{}

func (c Context) Set(req *http.Request, v interface{}) {
    c[req] = v
}

func (c Context) Get(req *http.Request) interface{} {
    return c[req]
}

As you see, given a http.Request, we can store values in a context. Later we can retrieve these values using the same Context and the same http.Request. mux uses a global Context to store the vars parsed in routing process so that you can use the standard http.request. But, because httputil.ReverseProxy passes a copy of the actual request and Context links values by request, this new Request has no values in the Context.

To fix it, you can implement your own ReverseProxy based on httputil.ReverseProxy:

type MyReverseProxy struct {
    httputil.ReverseProxy
    Director func(inr, outr *http.Request)
}

func (p *MyReverseProxy) ServeHTTP(rw http.ResponseWriter, inr *http.Request) {
    p.ReverseProxy.Director = func(outr *http.Request) {
        p.Director(inr, outr)
    }
    p.ReverseProxy.ServeHTTP(rw, inr)
}

func NewProxy(r *route) http.Handler {
    director := func(inr, outr *http.Request) {
        out, _ := url.Parse(r.base)

        outr.URL.Scheme = out.Scheme
        outr.URL.Host = out.Host
        outr.URL.Path = out.Path + "/" + mux.Vars(inr)["path"] 

        log.Printf("IN VARS: %#v\n", mux.Vars(inr)) // Now inr has proper vars
        log.Printf("OUT VARS: %#v\n", mux.Vars(outr))
    }
    return &MyReverseProxy{Director: director}

You can even use context and keep Director declaration:

type MyReverseProxy struct {
    httputil.ReverseProxy
    Director func(req *http.Request)
}

func (p *MyReverseProxy) ServeHTTP(rw http.ResponseWriter, inr *http.Request) {
    p.ReverseProxy.Director = func(outr *http.Request) {
        context.Set(outr, "in_req", inr)
        p.Director(outr)
    }
    p.ReverseProxy.ServeHTTP(rw, inr)
}

func NewProxy(r *route) http.Handler {
    director := func(outr *http.Request) {
        out, _ := url.Parse(r.base)

        inr := context.Get(outr, "in_req").(*http.Request)
        outr.URL.Scheme = out.Scheme
        outr.URL.Host = out.Host
        outr.URL.Path = out.Path + "/" + mux.Vars(inr)["path"]

        log.Printf("IN VARS: %#v\n", mux.Vars(inr)) // Now inr has proper vars
        log.Printf("OUT VARS: %#v\n", mux.Vars(outr))
    }
    return &MyReverseProxy{Director: director}
}

Both implementations seem tricky to me. They have to change httputil.ReverseProxy's Director in every call. So, I probably accept that mux is not a good choice here, and instead I will use some simpler solution:

var routes = []route{
    route{match: "/api/", base: "https://api.bar.com/5"},
    route{match: "/sales/", base: "https://sales.bar.com/3"},
}

func NewProxy(r *route) http.Handler {
    director := func(req *http.Request) {
        out, _ := url.Parse(r.base)

        req.URL.Scheme = out.Scheme
        req.URL.Host = out.Host
        req.URL.Path = out.Path + "/" + strings.TrimPrefix(req.URL.Path, r.match)
    }
    return &httputil.ReverseProxy{Director: director}
}

You can read mux source code to implement a complex solution based on regular expressions.

Upvotes: 5

Related Questions