In this article, we will learn about reverse proxies, its application scenarios and how to implement it in Golang.

Reverse proxies are servers that sit in front of a web server and forward requests from clients (such as web browsers) to the web server. They allow you to control the requests from the client and the responses from the server, and then we can use this feature to increase caching, do things to improve the security of the site, and so on.

Before we dive into more information about reverse proxies, let’s take a quick look at the differences between normal proxies (also known as forward proxies) and reverse proxies.

In forward proxy, the proxy retrieves data from another website on behalf of the original client. It sits in front of the client (browser) and ensures that no back-end server communicates directly with the client. All client requests are forwarded through the proxy, so the server only communicates with this proxy (the server will consider the proxy to be its client). In this case, the proxy can hide the real client.

forward proxy

On the other hand, the reverse proxy sits in front of the back-end server, ensuring that no client communicates directly with the server. All client requests are sent to the server through the reverse proxy, so that the client always communicates only with the reverse proxy and never directly with the actual server. In this case, the proxy can hide the back-end server. A few common reverse proxies are Nginx, HAProxy.

reverse proxy

Reverse Proxy Usage Scenarios

Load balancing: Reverse proxies can provide a load balancing solution, distributing incoming traffic evenly across different servers to prevent individual servers from being overloaded.

Preventing security attacks: Since the real back-end servers never need to expose public IPs, attacks such as DDoS can only be carried out against reverse proxies, which ensures that your resources are protected as much as possible in a network attack and that the real back-end servers are always safe.

Caching: Assuming that your actual server is far away from the user’s region, you can deploy a reverse proxy locally, which can cache website content and serve it to local users.

SSL encryption: Since SSL communication with each client consumes a lot of computing resources, you can use a reverse proxy to handle all SSL-related content and then free up valuable resources on your real server.

Golang implementation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import (
    "log"
    "net/http"
    "net/http/httputil"
    "net/url"
)

// NewProxy takes target host and creates a reverse proxy
// NewProxy 拿到 targetHost 后,创建一个反向代理
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
// ProxyRequestHandler 使用 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
    // 使用 proxy 处理所有请求到你的服务
    http.HandleFunc("/", ProxyRequestHandler(proxy))
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Yes, that’s right! That’s all you need to create a simple reverse proxy in Go. We create a single-host reverse proxy using the standard library net/http/httputil. Any requests arriving at our proxy server are proxied to a server located at http://my-api-server.com. If you are familiar with Go, the implementation of this code is obvious at a glance.

Modifying the response

The HttpUtil reverse proxy provides us with a very simple mechanism to modify the response we get from the server, this response can be cached or changed depending on your application scenario, let’s see how this should be done.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 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
}

As you can see in the modifyResponse method, we set the custom Header header. Likewise, you can read the body of the response, change it or cache it, and set it back to the client.

In modifyResponse, you can return an error (if you have an error processing the response), and if you have proxy.ErrorHandler set up, modifyResponse will automatically call ErrorHandler for error handling when it returns an error.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 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")
    }
}

Modify the request

You can also modify the request before it is sent to the server. In the example below, we will add a Header header to the request before it is sent to the server. Again, you can make any changes to the request before it is sent.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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 as mentioned earlier in the article, it has many application scenarios. You can customize it to suit your situation.