Implementing a Reverse Proxy API in Go

Implementing a Reverse Proxy API in Go

Achieve never-before speed with a middleman proxy server

ยท

5 min read

Introduction

If you have had any experience consuming APIs on the web, you would agree that one of the most frustrating issues you can experience is a Cross-Origin Resource Sharing error (CORS). I have previously written an article on how to fix CORS issues using a Reverse Proxy built with Nodejs, you can find the link to the article here.

In this article, I would be sharing how you can write the same Reverse Proxy from my previous article in Go. But first:

Why Go?

Golang is a statically-typed open-source programming language developed by Google in 2017 for building large-scale applications as lightweight microservices. Go is currently faster than Java, Nodejs and Python and is a relatively simple language to pick up (as it has only 26 keywords). Go is also multi-threaded as it can perform multiple tasks at the same time through Concurrency.

Now that we know why Go is suitable for use to build our Reverse Proxy, let's start writing some code!

Step 1: Setting up our Project

Install the latest version of Go by downloading it here

To set up our Go project, create a new folder and enter:

go mod init

This would create a go.mod file for tracking dependencies. Afterwards, create a main.go file within the same folder for your project.

Step 2: Installing Dependencies

Next, we need to add the dependencies we would be using in this project. There's no need to install them as these dependencies come out the box with Go.

The first few lines of your main.go project should now look like this:

package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
)

Step 3: Setup your Main Function

The func main() in your main.go is the part where all the magic happens. Like the previous API, our reverse proxy will have 3 endpoints:

A GET endpoint of our reverse proxy for making GET requests to our actual API with Authorization headers

A POST endpoint of our reverse proxy for making POST requests to our actual API with Authorization headers

A NOAUTH endpoint of our reverse proxy for making POST requests to our actual API that does not require Authorization headers

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprint(w, "<h1>My Go Reverse Proxy is Super-fast โšก</h1>")
    })
    http.HandleFunc("/api/get", ReverseProxyAPIGet)
    http.HandleFunc("/api/post", ReverseProxyAPIPost)
    http.HandleFunc("/api/noauth", ReverseProxyAPINoAuth)

    port := os.Getenv("PORT")
    if port == "" {
        port = "7660"
    }

    http.ListenAndServe(":"+port, nil)
}

Step 4: Writing our Controllers

Next, we'd like to write the controllers for each endpoint.

For the GET request:

func ReverseProxyAPIGet(w http.ResponseWriter, r *http.Request) {
    url := r.URL.Query().Get("url")

    client := &http.Client{}
    req, err := http.NewRequest("GET", url, nil)

    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    req.Header.Set("Content-Type", r.Header.Get("Content-Type"))
    req.Header.Set("Authorization", r.Header.Get("Authorization"))

    resp, err := client.Do(req)

    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)

    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    w.Write(body)
}

For the POST request:

func ReverseProxyAPIPost(w http.ResponseWriter, r *http.Request) {
    url := r.URL.Query().Get("url")

    client := &http.Client{}
    req, err := http.NewRequest("POST", url, r.Body)

    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    req.Header.Set("Content-Type", r.Header.Get("Content-Type"))
    req.Header.Set("Authorization", r.Header.Get("Authorization"))

    resp, err := client.Do(req)

    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)

    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    w.Write(body)
}

For the NOAUTH request:

func ReverseProxyAPINoAAuth(w http.ResponseWriter, r *http.Request) {
    url := r.URL.Query().Get("url")
    client := &http.Client{}
    req, err := http.NewRequest("POST", url, r.Body)

    w.Header().Set("Content-Type", "application/json")

    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    req.Header.Set("Content-Type", r.Header.Get("Content-Type"))
    req.Header.Set("Channel", "Web")

    resp, err := client.Do(req)

    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)

    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    w.Write(body)
}

Step 4: Implementing CORS

Now that we have successfully written our controllers, we can now proceed to write a function that would allow access to our Reverse proxy from any origin. We don't want our Reverse Proxy to be giving us CORS errors like our actual API now would we? Because what would be the point of writing the Reverse proxy ๐Ÿ˜‰.

To allow CORS in our Reverse Proxy, we can use the function below:

func withCORS(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
        w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, Channel")

        if r.Method == "OPTIONS" {
            w.WriteHeader(http.StatusOK)
            return
        }

        h.ServeHTTP(w, r)
    }
}

Then we can wrap our controllers in the func main to allow for CORS. So our func main would be re-written as this:

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprint(w, "<h1>My Go Reverse Proxy is Super-fast โšก</h1>")
    })
    http.HandleFunc("/api/get", withCORS(ReverseProxyAPIGet))
    http.HandleFunc("/api/post", withCORS(ReverseProxyAPIPost))
    http.HandleFunc("/api/noauth", withCORS(ReverseProxyAPINoAuth))

    port := os.Getenv("PORT")
    if port == "" {
        port = "7660"
    }

    http.ListenAndServe(":"+port, nil)
}

Step 5: Rounding up and making an API call

Now, we're done with writing the code. To use this newly written API as a middleman proxy, you can call your API like this:

curl -H "Authorization: Bearer YOUR_TOKEN" -H "Accept: application/json" -H "Content-Type: application/json" -X GET http://localhost:7660/api/get?url=YOUR_ACTUAL_API


curl -H "Authorization: Bearer YOUR_TOKEN" -H "Accept: application/json" -H "Content-Type: application/json" -X POST -d '{"param1":"value1", "param2":"value2"}' http://localhost:7660/api/post?url=YOUR_ACTUAL_API

curl -H "Accept: application/json" -H "Content-Type: application/json" -X POST -d '{"param1":"value1", "param2":"value2"}' http://localhost:7660/api/noauth?url=YOUR_ACTUAL_API

And that's it! Our Reverse proxy API written in Go is ready for consumption and deployment

Conclusion

In this article, we successfully wrote a Reverse Proxy in Go that enables us to pass an API with CORS issues as a URL query and then make an API call on our behalf without returning a CORS response. To read more about CORS and Reverse proxies, you can read my previous article which can be found here.

Happy Hacking โœŒ๐Ÿพ

ย