Simple and Powerful ReverseProxy in Go – Josh Software

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, let’s quickly understand the difference between a normal proxy (aka forward proxy) and a reverse proxy.

In Forward Proxy, proxy retrieves data from another website on the behalf of the 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 the server via reverse proxy and hence the 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 that 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 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 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! That’s all it takes to create a simple proxy in Go. We used the 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 the 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 a proxy.ErrorHandler. ErrorHandler is automatically called if you set an 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 the below example, we are adding a header before sending it to the server. Similarly, you can make any changes to the request before sending it.

// 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))
}

The proxy is very powerful and can be used for multiple use cases as explained above. You can try customizing 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 other gophers! Thanks a lot for reading.

Leave a Comment

Your email address will not be published.