Avatar (Fabio Alessandro Locati|Fale)'s blog

CORS headers with gRPC-Gateway

July 28, 2021

A few years ago, I wrote a blog post on managing CORS headers with Negroni. Lately, I’ve created a new API server that needed to be accessible from the browser, but this time I used a different technology, more precisely gRPC-Gateway.

Few months after I wrote that blog post, I stopped writing new REST services by hand. I did not rewrite all the services that used the old paradigm just because they needed a fix or a new feature, but for all new services, I moved to gRPC with gRPC-Gateway.

If there is interest, I can discuss how I use gRPC, gRPC-Gateway, and related technologies, but here I’d like to focus on the gRPC-Gateway CORS header topic. There are a few guides online on the topic, but I’ve found them lacking for a reason or another, and that is why I decided to publish here my take on the matter.

In all cases I’ve seen, people create a Mux server from the runtime subpackage of gRPC-Gateway, so the code would be something like:

package main
import (
    "net/http"

    "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
)

func main() {
    mux := runtime.NewServeMux()
    // register the gRPC Service Handler(s) to the mux
    srv := &http.Server{
        Addr:         ":443",
        Handler:      mux,
    }
    srv.ListenAndServe()
}

This code is simplified code, and often there will also be TLS involved and error management. Though, the code structure is usually like that.

Now, the interesting part of this is that mux is an http.Handler. This means that the CORS injection will be seamless to integrate if we can create an http.Handler wrapper to inject the headers.

To do so, we will need a function that takes a single http.Handler as a parameter and returns another http.Handler. Mising this idea with what we did in the Negroni blog post, we get:

func allowedOrigin(origin string) bool {
    if viper.GetString("cors") == "*" {
        return true
    }
    if matched, _ := regexp.MatchString(viper.GetString("cors"), origin); matched {
        return true
    }
    return false
}

func cors(h http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if allowedOrigin(r.Header.Get("Origin")) {
            w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin"))
            w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE")
            w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, Authorization, ResponseType")
        }
        if r.Method == "OPTIONS" {
            return
        }
        h.ServeHTTP(w, r)
    })
}

As you can see in line 2 of the previous snippet, I like to set the CORS regexp or value using viper. The reason I use viper for this is that it allows me to have a different configuration file per environment. When I need to promote a build from one environment to the next, I only need to substitute the binary file, and all configurations will be properly picked up. If you prefer a different way of setting it, you’ll need to change that line to reflect this preference.

To integrate the functions with the HTTP server, it will be enough to slightly change the srv definition adding the wrapper to the Handler value:

    srv := &http.Server{
        Addr:         ":443",
        Handler:      cors(mux),
    }

In this way, the whole main becomes:

package main
import (
    "net/http"
    "regexp"

    "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
    "github.com/spf13/viper"
)

func allowedOrigin(origin string) bool {
    if viper.GetString("cors") == "*" {
        return true
    }
    if matched, _ := regexp.MatchString(viper.GetString("cors"), origin); matched {
        return true
    }
    return false
}

func cors(h http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if allowedOrigin(r.Header.Get("Origin")) {
            w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin"))
            w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE")
            w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, Authorization, ResponseType")
        }
        if r.Method == "OPTIONS" {
            return
        }
        h.ServeHTTP(w, r)
    })
}

func main() {
    mux := runtime.NewServeMux()
    // register the gRPC Service Handler(s) to the mux
    srv := &http.Server{
        Addr:         ":443",
        Handler:      cors(mux),
    }
    srv.ListenAndServe()
}

I think this is an elegant solution to the issue that also allows the proper level of flexibility.