Reverse Proxy in Go

October 22, 2024

What’s a reverse proxy?

Nah, let’s skip this part. Knowing you’ve landed here means that you’ve understand what you’d want to build.

Reverse Proxy in httputil

Go has a good collection of standard library, this includes a Reverse Proxy.

With this, we could build a simple configurable reverse proxy.

Getting Started

Create an empty project.

$ mkdir reverse-proxy; cd reverse-proxy;
$ go mod init reverse-proxy;
$ go work init .;

Let’s read the configuration file first!

Here’s a sample configuration file we’d like to read.

config.yml

host: localhost     
port: 3000
resources:
  - name: google
    path: /search
    dest: https://www.google.com/

To read the YML file, we’ll be using yaml.v3

go get gopkg.in/yaml.v3

main.go

package main;

import (
	"log"
	"os"

	"gopkg.in/yaml.v3"
)

type Endpoint struct {
	Name string `yaml:name`
	Path string `yaml:path`
	Dest string `yaml:dest`
}

type Config struct {
	Host      string     `yaml:host`
	Port      int        `yaml:port`
	Resources []Endpoint `yaml:endpoints`
}

func main() {
	t := Config{}

	config, err := os.ReadFile("config.yml")

	if err != nil {
		log.Fatalln("Could not found configuration file")
	}

	err = yaml.Unmarshal([]byte(config), &t)

	if err != nil {
		log.Fatalln("Invalid configuration file")
	}

	log.Println(t.Host)
}

Creating our HTTP server

Started in Go 1.22, Go offers an enhanced routing. Learn more here.

import (
+	"fmt",
+	"net/http",
	"log"
	"os"

	"gopkg.in/yaml.v3"
)
......................................................
func main() {
	t := Config{}

	config, err := os.ReadFile("config.yml")

	if err != nil {
		log.Fatalln("Could not found configuration file")
	}

	err = yaml.Unmarshal([]byte(config), &t)

	if err != nil {
		log.Fatalln("Invalid configuration file")
	}
  
+	mux := http.NewServeMux()
+
+	for i := 0; i < len(t.Resources); i++ {
+		curr := t.Resources[i]
+
+		log.Printf("Adding Proxy for [%s] Path \"%s\" to \"%s\"", curr.Name, curr.Path, curr.Dest)
+
+		mux.HandleFunc(curr.Path, func(w http.ResponseWriter, r *http.Request) {
+			fmt.Fprintf(w, curr.Dest)
+		})
+	}
+
+	http.ListenAndServe(fmt.Sprintf("%s:%d", t.Host, t.Port), mux)

	log.Println(t.Host)
}

These changes should give us something like this

HTTP Server - First Run

Adding the proxy handler

import (
	"fmt"
	"log"
+	"net/http"
+	"net/http/httputil"
+	"net/url"
	"os"
+	"strings"

	"gopkg.in/yaml.v3"
)
......................................................
	for i := 0; i < len(t.Resources); i++ {
		curr := t.Resources[i]

		log.Printf("Adding Proxy for [%s] Path \"%s\" to \"%s\"", curr.Name, curr.Path, curr.Dest)

		mux.HandleFunc(curr.Path, func(w http.ResponseWriter, r *http.Request) {
-		fmt.Fprintf(w, curr.Dest)
+			proxy := httputil.ReverseProxy{
+				Rewrite: func(req *httputil.ProxyRequest) {
+					p, _ := url.Parse(curr.Dest)
+					p.Path = strings.TrimRight(strings.TrimLeft(r.URL.Path, curr.Path), "/")
+
+					req.SetXForwarded()
+					req.Out.Header.Add("Custom-Header", "Value")
+
+					log.Printf("[%s] %s -> %s", curr.Dest, r.URL, p.Path)
+					req.SetURL(p)
+				},
+			}
+
+			proxy.ServeHTTP(w, r)
+		})
}

Let’s try it now!

Reverse Proxy in Go

Conclusion

This is a very simple kickstart project to start of your journey into Reverse Proxy. I hope you’d find this useful. Next step is to introduce a dynamic path matching as this currently throws page not founds for non-matching paths.

To see the full project in this tutorial click here.