How to Handle CORS in Gin with Custom Configuration: Complete Middleware Guide

Use the gin-contrib/cors package to generate a configurable gin.HandlerFunc via cors.New(config), then register it globally or per-group using router.Use() to intercept requests, validate origins against your whitelist, and automatically handle OPTIONS preflight responses.

Handling Cross-Origin Resource Sharing (CORS) properly is essential for any Go web service that accepts browser-based requests. While the gin-gonic/gin framework does not ship with built-in CORS functionality, it provides a flexible middleware architecture that makes it straightforward to handle CORS in Gin with custom configuration using the official community package.

Understanding Gin's Middleware Architecture

Gin processes requests through a HandlersChain assembled for every matched route. According to the source code in routergroup.go, this chain consists of three tiers:

  1. Global middleware registered with router.Use(...) – executes for every incoming request.
  2. Group-level middleware attached to a RouterGroup – executes only for routes sharing that prefix.
  3. Route-specific middleware supplied during route registration.

CORS is implemented as middleware that intercepts the request before your route handler executes. It inspects the Origin header, applies your security policy, sets the necessary response headers (Access-Control-Allow-Origin, Access-Control-Allow-Methods, etc.), and may short-circuit the chain for preflight OPTIONS requests.

Installing the Official CORS Package

Gin relies on the gin-contrib/cors repository for CORS functionality. Install it alongside Gin:

go get github.com/gin-gonic/gin
go get github.com/gin-contrib/cors

Implementing Custom CORS Configuration

The cors.Config struct in cors.go exposes granular controls for origins, methods, headers, credentials, and preflight caching. You convert this configuration into middleware using cors.New(config), which returns a standard gin.HandlerFunc.

Global CORS Setup

Register CORS middleware at the router level to apply a uniform policy across all endpoints:

package main

import (
	"time"
	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	corsConfig := cors.Config{
		AllowOrigins:     []string{"https://example.com", "https://api.example.com"},
		AllowMethods:     []string{"GET", "POST", "PUT", "PATCH", "DELETE"},
		AllowHeaders:     []string{"Origin", "Content-Type", "Authorization"},
		ExposeHeaders:    []string{"Content-Length"},
		AllowCredentials: true,
		MaxAge:           12 * time.Hour,
	}

	r.Use(cors.New(corsConfig))

	r.GET("/data", func(c *gin.Context) {
		c.JSON(200, gin.H{"data": "response"})
	})

	r.Run(":8080")
}

Key implementation details from cors.go:

  • AllowOrigins supports exact strings, wildcards (*), or sub-domain patterns.
  • MaxAge defines how long browsers may cache preflight results.
  • AllowCredentials: true permits cookies and authorization headers in cross-origin requests.

Route Group Specific Policies

Because RouterGroup.Use() in routergroup.go accepts the same HandlerFunc interface, you can attach different CORS configurations to distinct API segments:

api := r.Group("/api")
api.Use(cors.New(cors.Config{
	AllowOrigins: []string{"https://client-app.com"},
	AllowMethods: []string{"GET", "POST"},
}))

admin := r.Group("/admin")
admin.Use(cors.New(cors.Config{
	AllowOrigins:     []string{"https://admin.example.com"},
	AllowMethods:     []string{"GET", "POST", "DELETE"},
	AllowCredentials: true,
}))

This approach isolates public API CORS rules from administrative endpoints without code duplication.

Manual CORS Implementation

For scenarios requiring dynamic origin validation logic that exceeds the Config struct capabilities, implement a custom gin.HandlerFunc:

import (
	"net/http"
	"strings"
)

func CustomCORSMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		origin := c.Request.Header.Get("Origin")
		
		// Allow any sub-domain of example.com
		if strings.HasSuffix(origin, ".example.com") {
			c.Header("Access-Control-Allow-Origin", origin)
			c.Header("Access-Control-Allow-Methods", "GET,POST,OPTIONS")
			c.Header("Access-Control-Allow-Headers", "Content-Type,Authorization")
			
			if c.Request.Method == http.MethodOptions {
				c.AbortWithStatus(http.StatusNoContent)
				return
			}
		}
		
		c.Next()
	}
}

Register this with r.Use(CustomCORSMiddleware()) exactly as you would the official package.

Internal Mechanics of CORS Handling

The gin-contrib/cors middleware operates as follows, based on the implementation in cors.go:

  1. Configuration Parsing: The New(config) function validates your Config struct and returns a closure matching the func(*gin.Context) signature required by Gin's middleware chain.

  2. Origin Validation: For each request, the handler extracts the Origin header and matches it against AllowedOrigins. It supports exact matches, the wildcard *, and pattern matching for sub-domains.

  3. Header Injection: Upon a successful match, it writes the configured CORS headers to the response writer before calling c.Next() to proceed to subsequent handlers.

  4. Preflight Termination: When the request method is OPTIONS and the Access-Control-Request-Method header is present, the middleware validates the preflight request. If valid, it calls c.AbortWithStatus(http.StatusNoContent) (HTTP 204), preventing any downstream route handlers from executing and avoiding unnecessary processing.

Summary

  • Gin's middleware architecture in routergroup.go supports CORS at global, group, or route levels via the Use() method.
  • The official gin-contrib/cors package provides a production-ready Config struct and New() constructor to generate gin.HandlerFunc middleware.
  • Custom configurations control allowed origins, methods, headers, credentials, and preflight caching duration.
  • The middleware automatically handles OPTIONS preflight requests by aborting the chain with status 204, ensuring compliant browser behavior.
  • For complex logic, implement manual middleware using c.Header() and c.AbortWithStatus().

Frequently Asked Questions

Does Gin include built-in CORS middleware?

No. The gin-gonic/gin repository intentionally keeps the core framework minimal. CORS functionality resides in the community-maintained gin-contrib/cors package, which follows Gin's middleware conventions and integrates seamlessly with the HandlersChain system defined in routergroup.go.

How do I configure CORS to allow credentials?

Set AllowCredentials: true in your cors.Config struct. When enabled, the middleware sets the Access-Control-Allow-Credentials: true header. You must also specify explicit origins in AllowOrigins (wildcard * is incompatible with credentials per the CORS specification). Example:

config := cors.Config{
	AllowOrigins:     []string{"https://trusted-site.com"},
	AllowCredentials: true,
}

Can I apply different CORS rules to different API endpoints?

Yes. Use RouterGroup instances to scope middleware. As shown in routergroup.go, each group maintains its own middleware chain. Attach distinct cors.New(config) calls to different groups (e.g., /api vs /admin), or apply middleware directly to individual route definitions for endpoint-specific policies.

Why do OPTIONS preflight requests return 404 errors?

This occurs when no route handler matches the OPTIONS method, or when CORS middleware is not registered early enough in the chain. Ensure you register cors.New(config) globally or within the specific group before defining routes. The middleware intercepts OPTIONS requests and aborts with status 204 before the router attempts to match a non-existent OPTIONS handler.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →