Build reverse proxy server in Go
b0r
Posted on December 7, 2021
Learn how to build a simple reverse proxy server in Go.
Table of Contents:
What is a Proxy Server
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]
By H2g2bob - Own work, CC0, https://commons.wikimedia.org/w/index.php?curid=16337577
Proxy Server types
Forward Proxy
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 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
Step 1: Create origin server
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".
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:
go run main
Use curl
command to validate origin server works as expected:
% 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:
[origin server] received request at: 2021-12-07 20:08:44.302807 +0100 CET m=+518.629994085
Step 2: Create a reverse proxy server
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:
go run main
Use curl
command to validate reverse proxy works as expected:
% 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:
[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)
Next, we will update reverse proxy to forward a client request to the origin server. Update main
function as follows:
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:
go run main
Use curl
command to validate reverse proxy works as expected:
% 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:
[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:
received request: 2021-12-07 20:37:30.290371 +0100 CET m=+97.775715418
Step 4: Copy Origin Server Response
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:
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:
go run main
Use curl
command to validate reverse proxy works as expected:
% 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:
[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:
received request: 2021-12-07 20:42:07.657175 +0100 CET m=+375.150991460
Common errors
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, requestURI
should always be set to ""
before trying to send a request.
Conclusion
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. 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 Clayton on Unsplash
Posted on December 7, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.