Simple and Powerful ReverseProxy in Go

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.

2 thoughts on “Simple and Powerful ReverseProxy in Go

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.