
In this article we will learn about reverse proxy, where to use it and how to implement it in Golang.
A reverse proxy is a server that sits in front of web servers and forwards client (e.g. web browser) requests to web servers. They give you control over the request from clients and responses from the servers and then we can use that to leverage benefits like caching, increased security and many more.
Before we learn more about reverse proxy, lets quickly understand the difference between a normal proxy (aka forward proxy) and reverse proxy.
In Forward Proxy, proxy retrieves data from another website on the behalf of original client. It sits in front of a client (your browser) and ensures that no backend server ever communicates directly with the client. All the client requests go through the forward proxy and hence the server only communicates with that proxy (assuming proxy is its client). In this case, the proxy masks the client.
On the other hand, a Reverse Proxy sits in front of backend servers and ensures that no client ever communicates directly with the servers. All the client requests go to server via reverse proxy and hence client is always communicating to reverse proxy and never with the actual server. In this case, the proxy masks the backend servers. Few examples of reverse proxy are Nginx Reverse proxy, HAProxy.
Reverse Proxy Use cases
Load balancing: a reverse proxy can provide a load balancing solution which will distribute the incoming traffic evenly among the different servers to prevent any single server from becoming overloaded
Preventing security attacks: since the actual web-servers never need to expose their public IP address, attacks such as DDoS can only target the reverse proxy which can be secured with more resources to fend off the cyber attack. Your actual servers are always safe.
Caching: Let’s say your actual servers are in a region far from your users, you can deploy regional reverse proxies which can cache content and serve to local users.
SSL encryption: As SSL communication with each client is computationally expensive, using a reverse proxy it can handle all your SSL related stuff and then freeing up valuable resources on your actual servers.
Golang Implementation
package main
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
)
// NewProxy takes target host and creates a reverse proxy
func NewProxy(targetHost string) (*httputil.ReverseProxy, error) {
url, err := url.Parse(targetHost)
if err != nil {
return nil, err
}
return httputil.NewSingleHostReverseProxy(url), nil
}
// ProxyRequestHandler handles the http request using proxy
func ProxyRequestHandler(proxy *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
proxy.ServeHTTP(w, r)
}
}
func main() {
// initialize a reverse proxy and pass the actual backend server url here
proxy, err := NewProxy("http://my-api-server.com")
if err != nil {
panic(err)
}
// handle all requests to your server using the proxy
http.HandleFunc("/", ProxyRequestHandler(proxy))
log.Fatal(http.ListenAndServe(":8080", nil))
}
And yes! Thats all it takes to create a simple reverse proxy in Go. We used standard library “net/http/httputil” and created a single host reverse proxy. Any request to our proxy server is proxied to the backend server located at http://my-api-server.com
. The code is pretty much self-explanatory if you are from Go background.
Modifying the response
HttpUtil reverse proxy provides us a very simple mechanism to modify the response we got from the servers. This response can be cached or changed based on your use cases. Let’s see how we can make this change
// NewProxy takes target host and creates a reverse proxy
func NewProxy(targetHost string) (*httputil.ReverseProxy, error) {
url, err := url.Parse(targetHost)
if err != nil {
return nil, err
}
proxy := httputil.NewSingleHostReverseProxy(url)
proxy.ModifyResponse = modifyResponse()
return proxy, nil
}
func modifyResponse() func(*http.Response) error {
return func(resp *http.Response) error {
resp.Header.Set("X-Proxy", "Magical")
return nil
}
}
You can see in modifyResponse
method, we are setting a custom header. Similarly you can read the response body, make changes to it, cache it and then set it back for the client.
In ModifyResponse you can also return an error (if you encounter it while processing response) which then will be handled by proxy.ErrorHandler. ErrorHandler is automatically called if you set error inside modifyResponse
// NewProxy takes target host and creates a reverse proxy
func NewProxy(targetHost string) (*httputil.ReverseProxy, error) {
url, err := url.Parse(targetHost)
if err != nil {
return nil, err
}
proxy := httputil.NewSingleHostReverseProxy(url)
proxy.ModifyResponse = modifyResponse()
proxy.ErrorHandler = errorHandler()
return proxy, nil
}
func errorHandler() func(http.ResponseWriter, *http.Request, error) {
return func(w http.ResponseWriter, req *http.Request, err error) {
fmt.Printf("Got error while modifying response: %v \n", err)
return
}
}
func modifyResponse() func(*http.Response) error {
return func(resp *http.Response) error {
return errors.New("response body is invalid")
}
}
Modifying the request
You can also modify the request before sending it to the server. In below example we are adding a header before sending it to server. Similarly, you can make any changes to the request before sending.
// NewProxy takes target host and creates a reverse proxy
func NewProxy(targetHost string) (*httputil.ReverseProxy, error) {
url, err := url.Parse(targetHost)
if err != nil {
return nil, err
}
proxy := httputil.NewSingleHostReverseProxy(url)
originalDirector := proxy.Director
proxy.Director = func(req *http.Request) {
originalDirector(req)
modifyRequest(req)
}
proxy.ModifyResponse = modifyResponse()
proxy.ErrorHandler = errorHandler()
return proxy, nil
}
func modifyRequest(req *http.Request) {
req.Header.Set("X-Proxy", "Simple-Reverse-Proxy")
}
Complete code
package main
import (
"errors"
"fmt"
"log"
"net/http"
"net/http/httputil"
"net/url"
)
// NewProxy takes target host and creates a reverse proxy
func NewProxy(targetHost string) (*httputil.ReverseProxy, error) {
url, err := url.Parse(targetHost)
if err != nil {
return nil, err
}
proxy := httputil.NewSingleHostReverseProxy(url)
originalDirector := proxy.Director
proxy.Director = func(req *http.Request) {
originalDirector(req)
modifyRequest(req)
}
proxy.ModifyResponse = modifyResponse()
proxy.ErrorHandler = errorHandler()
return proxy, nil
}
func modifyRequest(req *http.Request) {
req.Header.Set("X-Proxy", "Simple-Reverse-Proxy")
}
func errorHandler() func(http.ResponseWriter, *http.Request, error) {
return func(w http.ResponseWriter, req *http.Request, err error) {
fmt.Printf("Got error while modifying response: %v \n", err)
return
}
}
func modifyResponse() func(*http.Response) error {
return func(resp *http.Response) error {
return errors.New("response body is invalid")
}
}
// ProxyRequestHandler handles the http request using proxy
func ProxyRequestHandler(proxy *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
proxy.ServeHTTP(w, r)
}
}
func main() {
// initialize a reverse proxy and pass the actual backend server url here
proxy, err := NewProxy("http://my-api-server.com")
if err != nil {
panic(err)
}
// handle all requests to your server using the proxy
http.HandleFunc("/", ProxyRequestHandler(proxy))
log.Fatal(http.ListenAndServe(":8080", nil))
}
Reverse proxy is very powerful and can be used for multiple use cases as explained above. You can try customising it as per your case and if you face any issues, I will be very happy to help you with that. If you found the article interesting, please share it so that it can reach to other gophers! Thanks a lot for reading.
Summary:
ReverseProxy is very powerful and can be used for multiple use cases as explained above. Although, you can try customising it as per your case, however, if any challenges come in your way during the process, hopefully this article will guide you to overcome such an issue.
How would you go about adding code for multiple requests and multiple hosts connections?
I used gin-gonic for routing . My route also have path /metrics. I am trying to proxy this request to a mock server . But I am receving 404.
So the flow:
http://localhost:8088/metrics –> reverseProxy(http://demo8218883.mockable.io/graphql)
Although I am receiving 404.
Any help would be appreciated.
Great article.
Please add a few examples with multiple API routes.
Also, add explanation for route name terminated with ‘/’, for instance, “/api1” is not same as “/api1/”
What’s the significance of trailing ‘/’?
This’ll add a great value to the article.
I had to add r.Host = target.Host in order for this to work with the Github GraphQL API. Any idea why? Is this particular to my case, or would this Host header issue come up in your example as well? Thanks.