A deep dive into what Cross-Origin Resource Sharing (CORS) is, why it exists, and how to correctly configure it in a Go web server using the Gin framework.
If you've ever built a web application with a separate frontend and backend, you've almost certainly run into this dreaded error in your browser's console:
Access to fetch at 'http://localhost:8080/api/v1/join' from origin 'http://localhost:3000' has been blocked by CORS policy...
This error is a rite of passage for web developers. It can be frustrating, and the knee-jerk reaction is often to find a quick fix to "disable" it. But what if I told you that CORS isn't an error to be disabled, but a crucial security feature to be understood and configured correctly?
Let's break down what CORS is, why it exists, and how we've implemented it properly in our Go project using the Gin framework.
The "Why": A Security Rule Called the Same-Origin Policy
To understand CORS, you first need to understand the Same-Origin Policy (SOP). It's a fundamental security rule built into every modern web browser.
Here's a simple analogy: Imagine your backend server is an exclusive, members-only club. The Same-Origin Policy is the club's main rule: "You can only make requests and get data from people already inside this club."
An "origin" is defined by the combination of a protocol (like http
), a hostname (like my-game.com
), and a port (like :8080
).
- A script on
https://my-game.com
requesting data fromhttps://my-game.com/api/status
is the same origin. The request is allowed. - A script on
https://my-game.com
requesting data fromhttps://api.my-game.com
is a different origin (subdomain differs). - A script on
http://localhost:3000
(your frontend) requesting data fromhttp://localhost:8080
(your backend) is a different origin (port differs).
When your frontend tries to make a request to your backend, the browser sees it's going to a different "club" and, by default, blocks it for security reasons. This prevents a malicious website, say evil.com
, from making authenticated requests to your bank's API (your-bank.com/api
) using your browser's cookies.
So, how do we allow our legitimate frontend to talk to our backend?
The "How": CORS (Cross-Origin Resource Sharing)
CORS is the mechanism that allows the "club" (your backend server) to tell the browser, "It's okay, I trust that other building (your frontend's origin). You can let their requests through."
It’s not about disabling security; it’s about creating a list of trusted friends.
The "Preflight" Request: A Polite Conversation
For any request that could modify data (like POST
, PUT
, DELETE
, or any request with custom headers like Content-Type: application/json
), the browser doesn't send the request immediately. Instead, it sends a "preflight" request.
The Browser Asks: Before sending the real
POST
request to/api/v1/join
, the browser sends a tiny, harmlessOPTIONS
request to the same URL. It's essentially asking for permission: "Hey server, I'm a script fromhttp://localhost:3000
. Are you okay with me sending aPOST
request with aContent-Type
header?"-
The Server Responds: Your backend server, if configured for CORS, sees this
OPTIONS
request and replies with a set of special headers. These headers are the server's answer:-
Access-Control-Allow-Origin: http://localhost:3000
("Yes, I allow requests from that specific origin.") -
Access-Control-Allow-Methods: GET, POST, OPTIONS
("Yes, I allow these HTTP methods.") -
Access-Control-Allow-Headers: Content-Type, Authorization
("Yes, I allow these headers to be sent.")
-
The Browser Decides: If the server's response headers grant permission, the browser then sends the actual
POST
request with the JSON data. If the server doesn't respond with these headers, the browser blocks the request and shows you the infamous CORS error.
CORS in Action: Our Go & Gin Implementation
This whole "conversation" is handled beautifully by middleware. In our project, we use the gin-contrib/cors
package. Let's look at the configuration in cmd/server/main.go
:
// --- CORS MIDDLEWARE SETUP ---
corsConfig := cors.Config{
// Here we list our "trusted friends"
AllowOrigins: getAllowedOrigins(cfg.Server.AllowedOrigins),
// The methods we permit
AllowMethods: []string{"GET", "POST", "OPTIONS"},
// The headers our frontend is allowed to send
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization", "Upgrade"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}
// Apply this CORS "policy" to all incoming requests
router.Use(cors.New(corsConfig))
// --- END OF CORS SETUP ---
When we call router.Use(cors.New(corsConfig))
, we are inserting that "security check" station at the very beginning of our request processing assembly line.
It intercepts every incoming request.
If it's a preflight OPTIONS
request, it automatically sends back the correct Access-Control-*
headers based on our corsConfig
.
If it's a regular GET
or POST
request, it adds the Access-Control-Allow-Origin
header to the response, confirming to the browser that the request was legitimate.
All of this happens before our actual API handlers, like handleRequestJoinNonce
, are even touched. This keeps our handler code clean and focused on business logic, while the middleware handles the cross-origin security policy.
Conclusion
CORS is not an error; it's a vital security feature of the web. Understanding the Same-Origin Policy and the preflight request flow turns a frustrating roadblock into a configurable part of your server's security posture. By using a middleware library like gin-contrib/cors
, you can implement a robust and secure policy with just a few lines of code, ensuring your frontend and backend can communicate safely and effectively.
Top comments (1)
Bullshit. Instead of avoiding CORS you teach people how to configure it.