Build reverse proxy server in Go
Tags: go, programming
Published: 2021-12-07 20:44:28

**Learn how to build a simple reverse proxy server in Go.**

Table of Contents:
1. [What is a Proxy Server](#what-is-a-proxy-server)
2. [Proxy Server types](#proxy-server-types)
  1. [Forward Proxy](fp)
  2. [Reverse Proxy] (rp)
3. [Reverse Proxy Implementation](#reverse-proxy-implementation)
  1. [Step 1: Create origin server](#s1)
  2. [Step 2: Create a reverse proxy server](#s2)
  3. [Step 3: Forward a client request to the origin server (via reverse proxy)](#s3)
  4. [Step 4: Copy origin server response to the client (via reverse proxy)](s4)
5. [Common errors](#common-errors)
6. [Conclusion](#conclusion)



##  What is a Proxy Server <a name="what-is-a-proxy-server"></a>

Proxy server is a server that provides a gateway between the client (Alice) and the origin server (Bob). Instead of connecting directly to the origin server (to fulfill a resource request), client directs the request to the proxy server, which evaluates the request and performs the required action on origin server on the client behalf (e.g. get current time). [1]



![Proxy concept](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l3r2t80ota6ngptue45a.png)
> By H2g2bob - Own work, CC0, https://commons.wikimedia.org/w/index.php?curid=16337577

## Proxy Server types <a name="proxy-server-types"></a>
### Forward Proxy <a name="fp"></a>

An ordinary forward proxy is an intermediate server that sits between the client and the origin server. In order to get content from the origin server, the client sends a request to the proxy naming the origin server as the target and the proxy then requests the content from the origin server and returns it to the client. The origin server is only aware of the proxy server and not the client (optional).

#### Use cases

A typical usage of a forward proxy is to provide Internet access to internal clients that are otherwise restricted by a firewall.
The forward proxy can also use caching to reduce network usage. 
In addition, forward proxy can also be used to hide clients IP address. [2]


### Reverse Proxy <a name="rp"></a>

A reverse proxy, by contrast, appears to the client just like an ordinary web server. No special configuration on the client is necessary. The client makes ordinary requests for content in the name-space of the reverse proxy. The reverse proxy then decides where to send those requests, and returns the content as if it was itself the origin.

#### Use cases

A typical usage of a reverse proxy is to provide Internet users access to a server that is behind a firewall (opposite of Forward Proxy).

Reverse proxies can also be used to balance load among several back-end servers, or to filter, rate limit or log incoming request, or to provide caching for a slower back-end server.

In addition, reverse proxies can be used simply to bring several servers into the same URL space. [2]


### Reverse Proxy Implementation <a name="reverse-proxy-implementation"></a>

#### Step 1: Create origin server <a name="s1"></a>

In order to test our reverse proxy, we first need to create and start a simple origin server. 
Origin server will be started at port `8081` and it will return a string containing the value "origin server response".

```go
  package main

  import (
    "fmt"
    "log"
    "net/http"
    "time"
  )

    func main() {
      originServerHandler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
        fmt.Printf("[origin server] received request at: %s\n", time.Now())
        _, _ = fmt.Fprint(rw, "origin server response")
        })

      log.Fatal(http.ListenAndServe(":8081", originServerHandler))
    }
```

##### Step 1 Test
Start the server:
```sh
go run main
```

Use `curl` command to validate origin server works as expected:

```sh
% curl -i localhost:8081
HTTP/1.1 200 OK
Date: Tue, 07 Dec 2021 19:32:00 GMT
Content-Length: 22
Content-Type: text/plain; charset=utf-8

origin server response%
```

In the terminal of the origin server you should see:
```sh
[origin server] received request at: 2021-12-07 20:08:44.302807 +0100 CET m=+518.629994085
```

#### Step 2: Create a reverse proxy server <a name="s2"></a>

```go
package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	
	reverseProxy := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
		fmt.Printf("[reverse proxy server] received request at: %s\n", time.Now())
	})

	log.Fatal(http.ListenAndServe(":8080", reverseProxy))
}
```

##### Step 2 Test

Start the server:
```sh
go run main
```

Use `curl` command to validate reverse proxy works as expected:

```sh
% curl -i localhost:8080
HTTP/1.1 200 OK
Date: Tue, 07 Dec 2021 19:17:45 GMT
Content-Length: 0
```

In the terminal of the reverse proxy server you should see:
```sh
[reverse proxy server] received request: 2021-12-07 20:09:44.302807 +0100 CET m=+518.629994085
```


#### Step 3: Forward a client request to the origin server (via reverse proxy) <a name="s3"></a>

Next, we will update reverse proxy to forward a client request to the origin server. Update `main` function as follows:

```go
func main() {

  // define origin server URL
	originServerURL, err := url.Parse("http://127.0.0.1:8081")
	if err != nil {
		log.Fatal("invalid origin server URL")
	}

	reverseProxy := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
		fmt.Printf("[reverse proxy server] received request at: %s\n", time.Now())

    // set req Host, URL and Request URI to forward a request to the origin server
		req.Host = originServerURL.Host
		req.URL.Host = originServerURL.Host
		req.URL.Scheme = originServerURL.Scheme
		req.RequestURI = ""
    
    // send a request to the origin server
		_, err := http.DefaultClient.Do(req)
		if err != nil {
			rw.WriteHeader(http.StatusInternalServerError)
			_, _ = fmt.Fprint(rw, err)
			return
		}
	})

	log.Fatal(http.ListenAndServe(":8080", reverseProxy))
}
```

##### Step 3 Test

Start the server:

```sh
go run main
```

Use `curl` command to validate reverse proxy works as expected:

```sh
% curl -i localhost:8080
HTTP/1.1 200 OK
Date: Tue, 07 Dec 2021 19:35:19 GMT
Content-Length: 0
```

In the terminal of the reverse proxy server you should see:
```sh
[reverse proxy server] received request at: 2021-12-07 20:37:30.288783 +0100 CET m=+4.150788001
```

In the terminal of the origin server you should see:
```sh
received request: 2021-12-07 20:37:30.290371 +0100 CET m=+97.775715418
```

#### Step 4: Copy Origin Server Response <a name="s4"></a>

Once we were able to proxy a client response to the origin server, we need to get the response from the origin server back to the client. To do that, update the main function as follows:

```go

    func main() {

  	// define origin server URL
  	originServerURL, err := url.Parse("http://127.0.0.1:8081")
  	if err != nil {
  		log.Fatal("invalid origin server URL")
  	}

  	reverseProxy := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
  		fmt.Printf("[reverse proxy server] received request at: %s\n", time.Now())

  		// set req Host, URL and Request URI to forward a request to the origin server
  		req.Host = originServerURL.Host
  		req.URL.Host = originServerURL.Host
  		req.URL.Scheme = originServerURL.Scheme
  		req.RequestURI = ""

      // save the response from the origin server
  		originServerResponse, err := http.DefaultClient.Do(req)
  		if err != nil {
  			rw.WriteHeader(http.StatusInternalServerError)
  			_, _ = fmt.Fprint(rw, err)
  			return
  		}

      // return response to the client
  		rw.WriteHeader(http.StatusOK)
  		io.Copy(rw, originServerResponse.Body)
  	})

  	log.Fatal(http.ListenAndServe(":8080", reverseProxy))

```

##### Step 4 Test

Start the server:

```sh
go run main
```

Use `curl` command to validate reverse proxy works as expected:

```sh
% curl -i localhost:8080
HTTP/1.1 200 OK
Date: Tue, 07 Dec 2021 19:42:07 GMT
Content-Length: 22
Content-Type: text/plain; charset=utf-8

origin server response%
```

In the terminal of the reverse proxy server you should see:
```sh
[reverse proxy server] received request at: 2021-12-07 20:42:07.654365 +0100 CET m=+5.612744376
```

In the terminal of the origin server you should see:
```sh
received request: 2021-12-07 20:42:07.657175 +0100 CET m=+375.150991460
```

## Common errors  <a name="common-erors"></a>

1. `Request.RequestURI can't be set in client request`

If the request is executed to the `http://localhost:8080/what-is-this` the `RequestURI` value will be `what-is-this`. 

According to Go docs [../src/net/http/client.go:217] (https://go.dev/src/net/http/client.go), `requestURI` should always be set to `""` before trying to send a request.

## Conclusion <a name="conclusion"></a>

In this article, reverse proxy explanation and its use cases were described. In addition, simple implementation of the reverse proxy in Go was provided.

Readers are encouraged to try improve this example by implementing copying of the response headers, adding `X-Forwarded-For` header and to implement HTTP2 support.

Also, don't forget to watch this awesome talk [FOSDEM 2019: How to write a reverse proxy with Go in 25 minutes.](https://www.youtube.com/watch?v=tWSmUsYLiE4) by Julien Salleyron on YT.


## Resources:
[1] https://en.wikipedia.org/wiki/Proxy_server#cite_note-apache-forward-reverse-5
[2] https://httpd.apache.org/docs/2.0/mod/mod_proxy.html#forwardreverse
[3] Photo by <a href="https://unsplash.com/@ibidsy?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Clayton</a> on <a href="https://unsplash.com/s/photos/gateway?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>