Skip to main content

Echo CORS Middleware

Introduction

When building web applications, you'll frequently encounter a security feature called Cross-Origin Resource Sharing (CORS). This feature controls how web pages in one domain can request resources from a server in a different domain. By default, web browsers block these cross-origin requests as a security measure.

Echo's CORS middleware provides a simple way to configure your server to handle cross-origin requests appropriately. This middleware is essential when you're developing APIs that need to be consumed by web applications hosted on different domains.

What is CORS?

CORS (Cross-Origin Resource Sharing) is a security mechanism implemented by browsers that restricts web pages from making requests to a domain different from the one that served the original web page.

For example:

  • If your frontend application is hosted at https://myapp.com
  • And your API is hosted at https://api.myapp.com
  • The browser will block direct requests from the frontend to the API unless proper CORS headers are configured

The Echo framework provides built-in middleware to handle these scenarios elegantly.

Setting Up Echo CORS Middleware

Basic Implementation

Let's start with a basic example of how to add CORS middleware to your Echo application:

go
package main

import (
"net/http"

"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)

func main() {
// Create a new Echo instance
e := echo.New()

// Add CORS middleware with default configuration
e.Use(middleware.CORS())

// Define a route that will be accessible cross-origin
e.GET("/api/data", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"message": "This data is accessible from different origins!",
})
})

// Start the server
e.Logger.Fatal(e.Start(":8080"))
}

With this configuration, your API endpoint /api/data will be accessible from any domain, as the default CORS middleware configuration allows all origins.

Custom CORS Configuration

In real-world applications, you'll want to restrict which domains can access your API. Here's how to customize the CORS middleware:

go
package main

import (
"net/http"

"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)

func main() {
e := echo.New()

// Custom CORS configuration
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"https://www.mywebsite.com", "https://app.mywebsite.com"},
AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete},
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization},
AllowCredentials: true,
MaxAge: 86400, // 24 hours
}))

// Routes
e.GET("/api/data", getData)

e.Logger.Fatal(e.Start(":8080"))
}

func getData(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"message": "This is restricted to specific origins",
})
}

In this example, only requests from https://www.mywebsite.com and https://app.mywebsite.com will be allowed to access the API.

Understanding CORS Configuration Options

The middleware.CORSConfig struct offers several options to customize your CORS behavior:

OptionDescriptionDefault
AllowOriginsList of allowed origins["*"] (all origins)
AllowMethodsHTTP methods allowed[GET, HEAD, PUT, POST, DELETE, PATCH]
AllowHeadersHeaders that can be used in the request[]
AllowCredentialsWhether cookies can be sent in cross-origin requestsfalse
ExposeHeadersHeaders that browsers are allowed to access[]
MaxAgeHow long the results of a preflight request can be cached (in seconds)0

Practical Example: Creating a Public API with Restricted Methods

Let's build an example API that allows read access from any domain, but restricts write operations to specific origins:

go
package main

import (
"net/http"

"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)

func main() {
e := echo.New()

// Configure CORS
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"*"}, // Allow all origins for GET
AllowMethods: []string{http.MethodGet}, // Only allow GET by default
AllowHeaders: []string{echo.HeaderContentType},
}))

// Public routes (read-only)
e.GET("/api/products", getProducts)

// Restricted group with custom CORS for write operations
admin := e.Group("/api/admin")
admin.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"https://admin.mycompany.com"}, // Only allow admin origin
AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete},
AllowHeaders: []string{echo.HeaderContentType, echo.HeaderAuthorization},
AllowCredentials: true,
}))

// Admin routes (read/write)
admin.POST("/products", createProduct)
admin.PUT("/products/:id", updateProduct)
admin.DELETE("/products/:id", deleteProduct)

e.Logger.Fatal(e.Start(":8080"))
}

func getProducts(c echo.Context) error {
products := []map[string]interface{}{
{"id": 1, "name": "Product 1", "price": 29.99},
{"id": 2, "name": "Product 2", "price": 39.99},
}
return c.JSON(http.StatusOK, products)
}

func createProduct(c echo.Context) error {
return c.JSON(http.StatusCreated, map[string]string{"status": "product created"})
}

func updateProduct(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"status": "product updated"})
}

func deleteProduct(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"status": "product deleted"})
}

This example demonstrates a common pattern where read operations are public, but write operations are restricted to specific domains.

Handling Preflight Requests

When browsers make cross-origin requests that might modify data (like POST, PUT, or DELETE), they first send a preflight OPTIONS request to check if the actual request is safe to send.

Echo's CORS middleware automatically handles these preflight requests, but it's useful to understand what's happening:

Browser -> OPTIONS /api/data -> Server
(Preflight Check)

Server responds with:
- Access-Control-Allow-Origin: https://allowed-domain.com
- Access-Control-Allow-Methods: GET, POST, PUT, DELETE
- Access-Control-Allow-Headers: Content-Type, Authorization

If preflight successful:
Browser -> POST /api/data -> Server
(Actual Request)

Common CORS Issues and Solutions

Issue 1: "No 'Access-Control-Allow-Origin' header is present"

This error occurs when the server doesn't include the necessary CORS headers.

Solution: Ensure your Echo CORS middleware is properly configured and applied to the correct routes.

Issue 2: Cookies not being sent with requests

Solution: Set AllowCredentials to true and specify exact origins (not wildcard *):

go
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"https://mywebsite.com"},
AllowCredentials: true,
}))

Issue 3: Custom headers not being accepted

Solution: Add your custom headers to the AllowHeaders list:

go
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"https://mywebsite.com"},
AllowHeaders: []string{echo.HeaderContentType, "X-Custom-Header"},
}))

Security Considerations

When implementing CORS, keep these security points in mind:

  1. Avoid using AllowOrigins: []string{"*"} in production for endpoints that modify data or return sensitive information
  2. Be specific with allowed origins - only include domains that truly need access
  3. Limit allowed methods to only what's necessary for each endpoint
  4. Consider using environment variables for allowed origins to easily switch between development and production settings
go
func main() {
e := echo.New()

// Get allowed origins from environment
allowedOrigins := strings.Split(os.Getenv("ALLOWED_ORIGINS"), ",")

e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: allowedOrigins,
// other options...
}))

// Routes...
}

Complete Working Example

Here's a complete working example that demonstrates different CORS configurations for different route groups:

go
package main

import (
"net/http"
"os"

"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)

func main() {
e := echo.New()

// Add logging middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())

// Public API - Allow all origins, GET only
publicAPI := e.Group("/api/public")
publicAPI.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"*"},
AllowMethods: []string{http.MethodGet},
}))

publicAPI.GET("/info", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"version": "1.0.0",
"status": "operational",
})
})

// Partner API - Allow specific origins, more methods
partnerAPI := e.Group("/api/partners")
partnerAPI.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"https://partner1.com", "https://partner2.com"},
AllowMethods: []string{http.MethodGet, http.MethodPost},
AllowHeaders: []string{echo.HeaderContentType, echo.HeaderAuthorization},
AllowCredentials: true,
}))

partnerAPI.GET("/data", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"message": "Partner data available here",
})
})

partnerAPI.POST("/feedback", func(c echo.Context) error {
return c.JSON(http.StatusCreated, map[string]string{
"message": "Feedback received",
})
})

// Internal API - Restricted to company domains
internalAPI := e.Group("/api/internal")
internalAPI.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"https://*.mycompany.com"},
AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete},
AllowHeaders: []string{echo.HeaderContentType, echo.HeaderAuthorization, "X-Company-Auth"},
ExposeHeaders: []string{"X-Response-Time"},
AllowCredentials: true,
MaxAge: 3600,
}))

internalAPI.GET("/dashboard", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"message": "Internal dashboard data",
})
})

// Get port from environment or default to 8080
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}

e.Logger.Fatal(e.Start(":" + port))
}

This example demonstrates different CORS configurations for:

  • Public API endpoints (open to all, read-only)
  • Partner API endpoints (limited to specific partners)
  • Internal API endpoints (restricted to company domains)

Summary

Echo's CORS middleware provides a flexible and powerful way to control cross-origin access to your API endpoints. By configuring the middleware appropriately, you can:

  • Allow cross-origin requests from specific domains
  • Restrict which HTTP methods are allowed for cross-origin requests
  • Control which headers can be included in requests
  • Determine whether credentials (cookies, HTTP authentication) can be included
  • Improve performance by setting appropriate cache times for preflight requests

Understanding and properly configuring CORS is crucial for creating secure and accessible web APIs with Echo.

Additional Resources

Exercises

  1. Create an Echo application with two route groups: one that allows CORS from any origin and another that only allows requests from "localhost".
  2. Modify the complete example to add a route group for mobile apps that only accepts requests from specific mobile app domains.
  3. Implement CORS middleware that reads allowed origins from a configuration file.
  4. Create a debugging middleware that logs all CORS-related headers received and sent by your application.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)