arrow Articles

CORS in Go (Golang) using rs/cors

date August 17, 2025

date 6 min read

Configuring CORS headers correctly in Go is easy, this tutorial shows you how.
Tim Armstrong Tim Armstrong, Founder, Consultant Engineer, Expert in Performance & Security
CORS in Go (Golang) using rs/cors

SHARE

linkedin share twitter share reddit share

In previous articles, we’ve covered what CORS is and the reverse proxy methods to Fixing “no ‘access-control-allow-origin’ header present”, but what do you do when you need a bit more flexibility?

This article guides you through the process of handling CORS in the backend.

This technique works for any Go HTTP Router framework that supports http.Handler. We’ll be using rs/cors for this tutorial as this is compatible with pretty much every HTTP Router Framework.

Naturally, as with programming in general, there are multiple ways of implementing the same thing, from a pure DIY approach to something prebuilt and packed nicely in a library. As indicated above, in this tutorial, we’ll be following the latter approach. But, before we get started with all that, let’s take a quick ReCap of the problem we’re trying to address.

What is CORS?

A diagram showing the handshake done by a browser when a cross-origin request is made

Cross-Origin Resource Sharing (CORS) is a protocol for relaxing the Same-Origin policy to allow scripts from one [sub]domain (Origin) to access resources at another. It does this via a preflight exchange of headers with the target resource.

When a script makes a request to a different [sub]domain than it originated from the browser first sends an OPTIONS request to that resource to validate that the resource is expecting requests from external code.

The OPTIONS request carries the Origin header, along with some other information about the request (check out the CORS explainer for more detail). The target resource then validates these details and (if valid) responds with its own set of headers describing what is permissible and how long to cache the preflight response for.

In our Fixing “no ‘access-control-allow-origin’ header present” article we did this validation and response generation with an Nginx Reverse-proxy and some RegEx. Which, while a good DevOps solution to the problem, lacks a degree of flexibility and relies heavily on our RegEx being safe. This Reverse-Proxy approach we covered in that article is a very good stopgap solution as it is easy to set up and requires no code changes. It does however have some significant shortcomings.

The biggest of which being the RegEx at the centre of that approach.

Risks of RegEx

A large number of CORS vulnerabilities are caused by misconfigured RegEx search strings in such reverse-proxy configurations.

For example, the RegEx string ^https\:\/\/.*example\.com$ might at first glance look like a valid solution to allowing us to have scripts from any subdomain of example.com contact our API over HTTPS. It certainly does work for such cases, as both www.example.com and blog.example.com would satisfy this RegEx.

However, what a lot of people miss is that it also accepts literally anything that starts with https:// ends in example.com. So, for example, even www.evilexample.com is a perfectly valid origin then according to the RegEx search string.

Obviously then, we want a solution that is similarly flexible (if not more flexible) while carrying a lower risk of misconfiguration.

Code-based solutions

Our next port of call then is to start looking at implementing this with as minimal a code change as possible.

Fortunately, every production-ready web server framework has some level of CORS support. In an earlier article, we covered a Python/FastAPI approach to this.

The pedagogical resource

For the rest of this tutorial, we’ll be extending the following nte/http based server stub:

func main() {

	mux := http.NewServeMux()
    mux.HandleFunc("POST /document", documentHandler)

	http.ListenAndServe(":8080", mux)
}

Here we’ll assume that the resource is hosted at api.example.com and the request is coming from www.example.com. The route of https://api.example.com/document accepts a POST request where it expects a JSON blob (the document) and some authentication Cookie.

To keep things simple we’ll assume that there is some handler documentHandler that appropriately validates the cookie and accepts the document. In reality, we’d want a separate middleware to handle the cookie validation so that we can be sure that all protected endpoints are processed properly, but we’ll gloss over that for this tutorial.

Adding basic CORS support

Adding CORS support using rs/cors is very easy.

To do this, the first thing you need to do is add "github.com/rs/cors" to your import.

Next, we’re going to extend our main() function as follows:

func main() {
	c := cors.New(cors.Options{
		AllowedOrigins: []string{"https://www.example.com"},
		AllowedMethods: []string{"POST"},
		AllowCredentials: true,
		MaxAge: 3600,
	})
	
	mux := http.NewServeMux()
    mux.HandleFunc("POST /document", documentHandler)

	http.ListenAndServe(":8080", c.Handler(mux))
}

So, let’s break down what’s going on here.

ListenAndServe

Lastly, we modified our http.ListenAndServe call by wrapping the mux object with our middleware engine c.Handler:

c.Handler(mux)

This is what does the heavy lifting here, intercepting our incoming queries to validate any preflight OPTIONS requests and validate the CORS headers before passing the request to our router.

Going further

So far we’ve replicated a similar result to our Nginx Reverse-Proxy solution, which is fine, but we’re working in code for a reason. We wanted to be able to dynamically add new Origins to our list of supported Access-Control-Allow-Origin responses (using a database perhaps).

So how do we make it dynamic?

Well, we could write our own wrapper function from scratch (the rs/cors framework is pretty straightforward, so bending a fork to our will is not too difficult), but in this case the library developers are one step ahead of us and have provided a dynamic Option AllowedOriginFunc(fn OriginValidator).

To use this option we simply write our own function that matches the signature:

type OriginValidator func(string) bool

Which might look something like this:

func originValidator(origin string) bool {
   valid := false
   err := pool.QueryRow("SELECT IF(origin=?, True, False) as 'valid' FROM origins", origin).Scan(&valid)
   if err != nil { return false }
   return valid
}

Here, our example originValidator uses SQL to compare the incoming origin against our database and if it’s found then it returns true if it’s not found then the function will return false.

The next thing we need to do is to simply replace our AllowedOrigins: []string{"https://www.example.com"}, option with the new validator.

	AllowedOriginFunc: originValidator,

How does this work then?

Behind the scenes, the main wrapper uses our function to validate if the Origin header is acceptable. Then, if it passes it, the wrapper will reflect the received origin into the access-control-allow-origin field.

This means it’s really important that our custom originValidator code is well tested and handles any potential exceptions safely. It is always better in this kind of code to fail-safe, as rejecting a potentially valid request is always preferable to accepting a potentially invalid one.

Further reading

If you hunger for more CORS knowledge check out our “What is CORS?” explainer article, or our Fixing “no ‘access-control-allow-origin’ header present” article.

If you’re looking for tutorials for other languages take a look at these:

Feel free to drop us a message if you’re looking for one that we haven’t covered yet.

arrow Articles overview