I am writing a simple reverse proxy with Golang. The code is listed below:
func NewMultiHostProxy(target_urls []string) gin.HandlerFunc {
var urls []*url.URL
for i := 0; i < len(target_urls); i++ {
target, err := url.Parse(target_urls[i])
if err != nil {
fmt.Errorf("Error parsing url")
return nil
}
urls = append(urls, target)
}
return func(c *gin.Context) {
director := func(req *http.Request) {
target := urls[rand.Int()%len(urls)]
r := c.Request
req = r
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.URL.Path = target.Path
req.Header.Set("X-GoProxy", "GoProxy")
if target.RawQuery == "" || req.URL.RawQuery == "" {
req.URL.RawQuery = target.RawQuery + req.URL.RawQuery
} else {
req.URL.RawQuery = target.RawQuery + "&" + req.URL.RawQuery
}
log.Print(req.URL)
}
proxy := &httputil.ReverseProxy{Director: director}
proxy.ServeHTTP(c.Writer, c.Request)
}
}
When I am trying to proxy one request to a REST api behind Nginx, Nginx always returns 404. However, if i directly access the REST api, it returns the result correctly. Here's my Nginx config:
server {
listen 80;
server_name myservername;
location /api {
proxy_pass http://127.0.0.1:5000;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
May I know how to debug this problem? Is it caused by Nginx configuration?
From https://golang.org/pkg/net/http/httputil/#ReverseProxy:
// Director must be a function which modifies
// the request into a new request to be sent
// using Transport. Its response is then copied
// back to the original client unmodified.
// Director must not access the provided Request
// after returning.
Director func(*http.Request)
That is the function used to construct ReverseProxy in &httputil.ReverseProxy{Director: director}
But your director
never modifies the http.Request
that original req
points to. It reassigns the pointer with req = r
. An irrelevant http.Request
is modified instead.
The issue is with request body. If you look into how Gin
implement get parameter, you will find that it open, read it and close it. So when you forward the request, the request body is empty.
I just ran into this same issue. I solved it by setting the Host. nginx uses the host(server_name) to decide which server on port 80 to serve.
req.Host = target.Host
// or
req.Host = ""
It can be set to an empty string because the url host was just set req.URL.Host = target.Host
https://golang.org/pkg/net/http/#Request
// For client requests Host optionally overrides the Host
// header to send. If empty, the Request.Write method uses
// the value of URL.Host. Host may contain an international
// domain name.
Host string
As a side note, you can create the proxy outside of the gin handler and call proxy.ServeHTTP(c.Writer, c.Request)
in the gin handler. The final result would look like this:
func NewMultiHostProxy(target_urls []string) gin.HandlerFunc {
var urls []*url.URL
for i := 0; i < len(target_urls); i++ {
target, err := url.Parse(target_urls[i])
if err != nil {
fmt.Errorf("Error parsing url")
return nil
}
urls = append(urls, target)
}
director := func(req *http.Request) {
target := urls[rand.Int()%len(urls)]
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.URL.Path = target.Path
req.Host = ""
req.Header.Set("X-GoProxy", "GoProxy")
if target.RawQuery == "" || req.URL.RawQuery == "" {
req.URL.RawQuery = target.RawQuery + req.URL.RawQuery
} else {
req.URL.RawQuery = target.RawQuery + "&" + req.URL.RawQuery
}
log.Print(req.URL)
}
proxy := &httputil.ReverseProxy{Director: director}
return func(c *gin.Context) {
proxy.ServeHTTP(c.Writer, c.Request)
}
}