Echo CORS Configuration
Introduction
When developing modern web applications, you will often have your frontend and backend running on different domains. For example, your frontend might be hosted at https://myapp.com
while your API is at https://api.myapp.com
. This separation creates security restrictions in browsers known as the Same-Origin Policy, which prevents JavaScript from making requests to a different domain than the one that served the web page.
Cross-Origin Resource Sharing (CORS) is a mechanism that allows servers to specify who can access their resources and how. In Echo framework, configuring CORS is essential for building APIs that need to be accessed by client applications from different origins.
This guide will walk you through configuring CORS in your Echo applications, from basic setup to advanced configurations.
Understanding CORS Basics
What is CORS?
CORS is an HTTP-header based mechanism that enables a server to indicate any origins (domain, scheme, or port) other than its own from which a browser should permit loading resources.
When a browser makes a cross-origin request:
- The browser sends an
Origin
header with the request - The server responds with
Access-Control-Allow-Origin
headers - The browser checks if the requesting origin is allowed
Types of CORS Requests
- Simple requests: GET, POST, or HEAD requests with certain content types
- Preflight requests: The browser sends an OPTIONS request before the actual request to check if it's safe to send
Basic CORS Configuration in Echo
Echo provides built-in middleware for handling CORS. Let's start with a basic implementation:
package main
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
e := echo.New()
// Apply CORS middleware
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"https://yourfrontend.com"},
AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete},
}))
// Routes
e.GET("/api/users", getUsers)
// Start server
e.Logger.Fatal(e.Start(":8080"))
}
func getUsers(c echo.Context) error {
users := []string{"John", "Jane", "Bob"}
return c.JSON(http.StatusOK, users)
}
In this example, we've configured our Echo server to:
- Allow requests only from
https://yourfrontend.com
- Accept GET, POST, PUT, and DELETE methods
Default CORS Middleware
Echo also provides a default CORS configuration which you can use for quick development:
// Default CORS middleware - allows all origins
e.Use(middleware.CORS())
The default CORS configuration allows all origins (*
), which is not recommended for production environments.
Advanced CORS Configuration
For more control over CORS behavior, Echo offers comprehensive configuration options:
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"https://app.example.com", "https://admin.example.com"},
AllowMethods: []string{http.MethodGet, http.MethodPut, http.MethodPost, http.MethodDelete},
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, "X-Custom-Header"},
ExposeHeaders: []string{"Content-Length", "X-Request-ID"},
AllowCredentials: true,
MaxAge: 86400, // Maximum age (in seconds) of the preflight request cache
}))
Key Configuration Options
Option | Description | Default |
---|---|---|
AllowOrigins | List of allowed origins | [*] |
AllowMethods | List of allowed HTTP methods | [GET, HEAD, PUT, POST, DELETE, PATCH] |
AllowHeaders | List of allowed HTTP headers | [] |
ExposeHeaders | Headers that browsers are allowed to access | [] |
AllowCredentials | Controls whether the browser includes credentials (cookies, auth headers) | false |
MaxAge | How long preflight results should be cached (in seconds) | 0 |
Handling Dynamic Origins
In some cases, you might need to dynamically determine which origins to allow. You can use the AllowOriginFunc
option for this:
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOriginFunc: func(origin string) (bool, error) {
// Check against a list of allowed domains or using a pattern
validOrigins := []string{
"https://app.example.com",
"https://admin.example.com",
// You could also allow all subdomains with regex checking
}
for _, o := range validOrigins {
if origin == o {
return true, nil
}
}
return false, nil
},
AllowMethods: []string{http.MethodGet, http.MethodPut, http.MethodPost, http.MethodDelete},
AllowCredentials: true,
}))
This approach gives you more flexibility in determining which origins to allow based on your own logic.
Real-World Example: API with Multiple Client Applications
Here's a practical example of an Echo API that serves multiple client applications:
package main
import (
"net/http"
"strings"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
e := echo.New()
// Environment-specific CORS configuration
var allowedOrigins []string
if isProd() {
// Production environment - strict origin checking
allowedOrigins = []string{
"https://main-app.example.com",
"https://admin.example.com",
"https://partner.example.com",
}
} else {
// Development environment
allowedOrigins = []string{
"http://localhost:3000",
"http://localhost:8000",
"http://127.0.0.1:3000",
}
}
// Configure CORS
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: allowedOrigins,
AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodPatch},
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization},
ExposeHeaders: []string{"X-Total-Count", "X-Pagination-Pages"},
AllowCredentials: true,
MaxAge: 3600, // 1 hour
}))
// API routes
api := e.Group("/api")
{
users := api.Group("/users")
users.GET("", listUsers)
users.POST("", createUser)
users.GET("/:id", getUser)
users.PUT("/:id", updateUser)
users.DELETE("/:id", deleteUser)
}
e.Logger.Fatal(e.Start(":8080"))
}
func isProd() bool {
// In a real-world scenario, you'd check environment variables
return false
}
// API handlers (simplified for the example)
func listUsers(c echo.Context) error {
return c.JSON(http.StatusOK, []map[string]string{
{"id": "1", "name": "John Doe"},
{"id": "2", "name": "Jane Smith"},
})
}
func createUser(c echo.Context) error {
return c.JSON(http.StatusCreated, map[string]string{"id": "3", "name": "New User"})
}
func getUser(c echo.Context) error {
id := c.Param("id")
return c.JSON(http.StatusOK, map[string]string{"id": id, "name": "User " + id})
}
func updateUser(c echo.Context) error {
id := c.Param("id")
return c.JSON(http.StatusOK, map[string]string{"id": id, "name": "Updated User"})
}
func deleteUser(c echo.Context) error {
return c.NoContent(http.StatusNoContent)
}
Testing Your CORS Configuration
It's important to test your CORS configuration to ensure it works correctly:
Using Curl
You can test preflight requests with curl
:
# Preflight request
curl -X OPTIONS https://your-api-url.com/api/resource \
-H "Origin: https://yourfrontend.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type" \
-v
Look for the Access-Control-Allow-Origin
header in the response, which should match your frontend origin.
In JavaScript
// Test from your frontend
fetch('https://your-api-url.com/api/users', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include' // Include if you're using cookies/auth
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('CORS error:', error));
Common CORS Issues and Solutions
Problem: "No 'Access-Control-Allow-Origin' header is present"
Solution: Ensure your Echo server is configured to allow the specific origin from which you're making the request.
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"https://yourfrontend.com"},
}))
Problem: Credentials not being sent or received
Solution: Set AllowCredentials: true
and make sure the specific origin is listed (not wildcard *
).
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"https://yourfrontend.com"},
AllowCredentials: true,
}))
On the client side, include credentials: 'include'
in your fetch options.
Problem: Custom headers not allowed
Solution: Include them in the AllowHeaders
list:
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"https://yourfrontend.com"},
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, "X-Custom-Header"},
}))
Security Best Practices
-
Avoid using wildcards in production: Instead of
AllowOrigins: []string{"*"}
, specify exact origins. -
Only allow necessary methods and headers: Don't enable methods or headers you don't actually need.
-
Be careful with
AllowCredentials
: When set totrue
, you cannot use a wildcard origin. -
Use environment-specific configurations: Different settings for development, staging, and production.
-
Consider rate limiting along with CORS: Protect your API from abuse with rate limiting middleware.
Summary
CORS configuration is essential for modern web applications that separate frontend and backend across different domains. Echo framework provides flexible middleware for handling CORS with various configuration options to suit different needs.
Key points to remember:
- Use specific origins instead of wildcards in production
- Configure only the methods and headers your application needs
- Be aware of the security implications of allowing credentials
- Test your CORS configuration thoroughly
- Use different configurations for different environments
CORS might seem complex at first, but with Echo's middleware, you can implement a secure and effective CORS policy for your API.
Additional Resources
Exercises
-
Set up an Echo API with CORS configuration that allows requests from
http://localhost:3000
andhttps://myapp.com
. -
Implement dynamic origin checking that allows all subdomains of
example.com
. -
Create a middleware that logs all CORS preflight requests to help with debugging.
-
Configure an Echo API with different CORS settings for different routes (hint: use group-specific middleware).
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)