Skip to content

CORS middleware duplicates headers when used in chained proxy servers #2853

@rLukoyanov

Description

@rLukoyanov

Description

When building a proxy server using Echo (golang) and enabling the built-in middleware.CORS() on each proxy layer, CORS headers are duplicated in the final response.

This happens in a common microservice / gateway setup where:

  • multiple Echo-based services act as proxies,
  • each service enables CORS middleware,
  • requests pass through several proxy layers before reaching the client.

As a result, the response contains duplicated CORS headers such as:

Access-Control-Allow-Origin: *
Access-Control-Allow-Origin: *
Vary: Origin
Vary: Origin
Image

This behavior breaks some browsers and violates expected HTTP header semantics.


Steps to Reproduce

  1. Create Service A (proxy) with Echo:

    func main() {
     e := echo.New()
    
     e.Use(middleware.Recover())
     e.Use(middleware.CORS())
    
     serviceAURL, err := url.Parse("http://localhost:8080")
     if err != nil {
     	e.Logger.Fatal(err)
     }
     serviceAProxy := httputil.NewSingleHostReverseProxy(serviceAURL)
    
     e.Any("/service-a/*", func(c echo.Context) error {
     	req := c.Request()
     	res := c.Response()
    
     	req.URL.Path = strings.TrimPrefix(req.URL.Path, "/service-a")
     	if req.URL.Path == "" {
     		req.URL.Path = "/"
     	}
    
     	serviceAProxy.ServeHTTP(res, req)
     	return nil
     })
    
     e.Logger.Fatal(e.Start(":8081"))
    }
  2. Create Service B (proxy) with the same setup:

    func main() {
     e := echo.New()
    
     e.Use(middleware.Recover())
     e.Use(middleware.CORS())
    
     e.GET("/", func(c echo.Context) error {
     	return c.JSON(http.StatusOK, map[string]string{
     		"service": "service-a",
     		"version": "1.0.0",
     		"status":  "running",
     	})
     })
    
     e.GET("/health", func(c echo.Context) error {
     	return c.JSON(http.StatusOK, map[string]string{
     		"status": "healthy",
     	})
     })
    
     e.Logger.Fatal(e.Start(":8080"))
    }
  3. Route traffic:

    Client → Service A → Service B
    
  4. Send a request from the browser or postman.


Expected Behavior

CORS headers should not be duplicated in the final response.
Ideally:

  • Echo should override existing CORS headers, or
  • CORS middleware should check if headers are already present before adding them.

Example:

Access-Control-Allow-Origin: *

Actual Behavior

Each proxy layer appends its own CORS headers, resulting in duplicates.


Why this is a problem

This is a realistic production scenario:

  • API Gateway
  • Internal proxy services
  • BFF layers

Disabling CORS in intermediate services is not always possible or desirable, especially when services are also used independently.


Environment

  • Echo version: v4.14.0
  • Go version: go1.24.0
  • OS: macOS

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions