CORS Middleware
Cross-Origin Resource Sharing (CORS) handling
Overview
CORS middleware handles Cross-Origin Resource Sharing requests, allowing web applications from different origins to access your API. It automatically handles preflight requests and sets appropriate CORS headers.
Import Path
import "github.com/primadi/lokstra/middleware/cors"
Usage
Basic Usage
// Allow all origins
router.Use(cors.Middleware([]string{"*"}))
Specific Origins
// Allow specific origins only
router.Use(cors.Middleware([]string{
"https://app.example.com",
"https://admin.example.com",
}))
Multiple Origins
allowedOrigins := []string{
"https://app.example.com",
"https://mobile.example.com",
"https://admin.example.com",
"http://localhost:3000", // Development
}
router.Use(cors.Middleware(allowedOrigins))
YAML Configuration
middlewares:
- type: cors
params:
allow_origins: ["*"]
# Or specific origins
- type: cors
params:
allow_origins:
- "https://app.example.com"
- "https://admin.example.com"
Features
Automatic Preflight Handling
CORS middleware automatically handles OPTIONS requests (preflight):
Client Request:
OPTIONS /api/users HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
Server Response:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Credentials Support
CORS middleware always sets Access-Control-Allow-Credentials: true, allowing cookies and authorization headers:
// Client-side JavaScript
fetch('https://api.example.com/users', {
credentials: 'include', // Include cookies
headers: {
'Authorization': 'Bearer token123'
}
})
Allowed Methods
Automatically allows these HTTP methods:
- GET
- POST
- PUT
- DELETE
- OPTIONS
Dynamic Headers
Echoes the Access-Control-Request-Headers from preflight requests, allowing any headers the client requests.
Examples
Allow All Origins (Development)
// During development, allow all origins
router.Use(cors.Middleware([]string{"*"}))
⚠️ Warning: Only use "*" in development. In production, specify exact origins.
Production Configuration
// Production: whitelist specific origins
func configureCORS() []string {
if os.Getenv("ENV") == "production" {
return []string{
"https://app.example.com",
"https://admin.example.com",
}
}
// Development: allow all
return []string{"*"}
}
router.Use(cors.Middleware(configureCORS()))
Multiple Environments
var allowedOrigins []string
switch os.Getenv("ENV") {
case "production":
allowedOrigins = []string{
"https://app.example.com",
"https://admin.example.com",
}
case "staging":
allowedOrigins = []string{
"https://staging.example.com",
"https://app.example.com",
}
default: // development
allowedOrigins = []string{"*"}
}
router.Use(cors.Middleware(allowedOrigins))
Per-Router Configuration
// Public API - allow all origins
publicRouter := lokstra.NewRouter()
publicRouter.Use(cors.Middleware([]string{"*"}))
// Internal API - restrict origins
internalRouter := lokstra.NewRouter()
internalRouter.Use(cors.Middleware([]string{
"https://internal.example.com",
}))
With Environment Variables
originsStr := os.Getenv("ALLOWED_ORIGINS")
var allowedOrigins []string
if originsStr == "" {
allowedOrigins = []string{"*"}
} else {
allowedOrigins = strings.Split(originsStr, ",")
}
router.Use(cors.Middleware(allowedOrigins))
Environment:
ALLOWED_ORIGINS="https://app.example.com,https://admin.example.com"
Dynamic Origin Validation
func getAllowedOrigins() []string {
// Fetch from database or config service
origins, err := configService.GetAllowedOrigins()
if err != nil {
log.Printf("Failed to get origins: %v", err)
return []string{} // Deny all if config fails
}
return origins
}
router.Use(cors.Middleware(getAllowedOrigins()))
Behavior
Origin Header Present
When Origin header is present in request:
- Check if origin is allowed
- If
"*"in config: allow any origin - Otherwise: check if origin is in whitelist
- If
- If origin is forbidden:
- Return
403 Forbidden - No CORS headers set
- Return
- If origin is allowed:
- Set
Access-Control-Allow-Origin: <origin> - Set
Access-Control-Allow-Credentials: true
- Set
- If OPTIONS request (preflight):
- Set
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS - Echo
Access-Control-Allow-Headersfrom request - Return
204 No Content
- Set
No Origin Header
If no Origin header is present (same-origin request), CORS headers are not set and request proceeds normally.
Best Practices
1. Use Specific Origins in Production
// ✅ Good - whitelist specific origins
router.Use(cors.Middleware([]string{
"https://app.example.com",
"https://admin.example.com",
}))
// 🚫 Bad - allows any origin in production
router.Use(cors.Middleware([]string{"*"}))
2. Include All Subdomains if Needed
// ✅ Good - explicit subdomains
router.Use(cors.Middleware([]string{
"https://app.example.com",
"https://api.example.com",
"https://admin.example.com",
"https://mobile.example.com",
}))
// 🚫 Bad - wildcard subdomains not supported
router.Use(cors.Middleware([]string{
"https://*.example.com", // Won't work
}))
3. Include Development Origins Conditionally
// ✅ Good - development origins only in dev
allowedOrigins := []string{
"https://app.example.com",
}
if os.Getenv("ENV") == "development" {
allowedOrigins = append(allowedOrigins,
"http://localhost:3000",
"http://localhost:8080",
)
}
router.Use(cors.Middleware(allowedOrigins))
4. Place CORS Early in Middleware Chain
// ✅ Good - CORS before authentication
router.Use(
recovery.Middleware(&recovery.Config{}),
cors.Middleware(allowedOrigins), // Early
jwtauth.Middleware(&jwtauth.Config{}),
)
// 🚫 Bad - auth blocks preflight requests
router.Use(
recovery.Middleware(&recovery.Config{}),
jwtauth.Middleware(&jwtauth.Config{}),
cors.Middleware(allowedOrigins), // Too late
)
5. Test Preflight Requests
# Test preflight
curl -X OPTIONS http://localhost:8080/api/users \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type, Authorization" \
-v
# Should return:
# Access-Control-Allow-Origin: https://app.example.com
# Access-Control-Allow-Credentials: true
# Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
# Access-Control-Allow-Headers: Content-Type, Authorization
Common Issues
Issue: Preflight Fails with 401
Problem: Authentication middleware blocks OPTIONS requests
Solution: Place CORS before authentication:
// ✅ Correct order
router.Use(
cors.Middleware(allowedOrigins), // First
jwtauth.Middleware(&jwtauth.Config{
SkipPaths: []string{"/auth/**"}, // Skip auth paths
}),
)
Issue: “No ‘Access-Control-Allow-Origin’ header”
Problem: Origin not in whitelist or CORS middleware not applied
Solution: Check configuration:
// Verify origin is in list
allowedOrigins := []string{
"https://app.example.com", // Must match exactly
}
// Check middleware is applied
router.Use(cors.Middleware(allowedOrigins))
Issue: Credentials Not Allowed
Problem: Browser blocks credentials with wildcard origin
Solution: Don’t use "*" when sending credentials:
// 🚫 Bad - wildcard doesn't work with credentials
router.Use(cors.Middleware([]string{"*"}))
// ✅ Good - specific origin
router.Use(cors.Middleware([]string{
"https://app.example.com",
}))
Issue: Wrong Origin Format
Problem: Origin must include protocol
Solution: Always include https:// or http://:
// 🚫 Bad - missing protocol
router.Use(cors.Middleware([]string{
"app.example.com",
}))
// ✅ Good - full origin
router.Use(cors.Middleware([]string{
"https://app.example.com",
}))
Performance
Overhead: ~500ns per request
Impact: Minimal - only performs header checks and string comparisons
Client-Side Examples
JavaScript Fetch
// Simple request
fetch('https://api.example.com/users', {
method: 'GET',
credentials: 'include', // Include cookies
})
// Request with headers (triggers preflight)
fetch('https://api.example.com/users', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
},
body: JSON.stringify({name: 'John'})
})
Axios
// Configure globally
axios.defaults.withCredentials = true
// Make request
axios.post('https://api.example.com/users', {
name: 'John'
}, {
headers: {
'Authorization': 'Bearer token123'
}
})
jQuery
$.ajax({
url: 'https://api.example.com/users',
type: 'POST',
xhrFields: {
withCredentials: true
},
headers: {
'Authorization': 'Bearer token123'
},
data: JSON.stringify({name: 'John'}),
contentType: 'application/json'
})
Testing
Test CORS in Go
func TestCORS(t *testing.T) {
router := lokstra.NewRouter()
router.Use(cors.Middleware([]string{
"https://app.example.com",
}))
router.GET("/test", func(c *request.Context) error {
return c.Api.Ok("success")
})
// Test with allowed origin
req := httptest.NewRequest("GET", "/test", nil)
req.Header.Set("Origin", "https://app.example.com")
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
assert.Equal(t, 200, rec.Code)
assert.Equal(t, "https://app.example.com",
rec.Header().Get("Access-Control-Allow-Origin"))
assert.Equal(t, "true",
rec.Header().Get("Access-Control-Allow-Credentials"))
}
func TestCORSForbidden(t *testing.T) {
router := lokstra.NewRouter()
router.Use(cors.Middleware([]string{
"https://app.example.com",
}))
// Test with forbidden origin
req := httptest.NewRequest("GET", "/test", nil)
req.Header.Set("Origin", "https://evil.com")
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
assert.Equal(t, 403, rec.Code)
}
See Also
- JWT Auth - Authentication middleware
- Recovery - Panic recovery
- Request Logger - Request logging
Related Guides
- Security Best Practices - Security patterns
- API Design - API design principles
- Frontend Integration - Frontend setup