Skip to content

CORS: the middleware misclassifies all OPTIONS requests as preflight requests #2534

Open
@jub0bs

Description

@jub0bs

Issue Description

Echo's CORS middleware misclassifies all OPTIONS requests as preflight requests, thereby unduly preventing requests from hitting user-registered OPTIONS endpoints.

I've previously discussed the general problem on my personal blog and how Echo suffers from it in issue #2510.

Checklist

  • Dependencies installed
  • No typos
  • Searched existing issues and docs

Expected behaviour

(For this section and the next, please refer to the server code below.)

Consider the OPTIONS requests resulting from the following two curl commands:

curl -v -XOPTIONS \
  localhost:8080/hello
curl -v -XOPTIONS \
  -H "Origin: https://example.com" \
  localhost:8080/hello

According to the Fetch standard, neither request is a preflight request, because

  • the first one lacks both an Origin header and an Access-Control-Request-Method header, and
  • the second one lacks an Access-Control-Request-Method header.

Therefore, those requests should get through the CORS middleware, exercise the handler registered on /hello, and get a response of this kind:

HTTP/1.1 204 No Content
Allow: GET, OPTIONS
Date: Mon, 23 Oct 2023 12:30:33 GMT

In contrast, an OPTIONS requests resulting from the following curl command is a bona fide preflight request; as such, it should be (and is) intercepted and handled by the CORS middleware:

curl -v -XOPTIONS \
  -H "Origin: https://example.com" \
  -H "Access-Control-Request-Method: PUT" \
  localhost:8080/hello
HTTP/1.1 204 No Content
Access-Control-Allow-Methods: PUT
Access-Control-Allow-Origin: *
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
-snip-

Actual behaviour

The first two aforementioned OPTIONS requests get intercepted and handled by the CORS middleware rather than by the handler registered on OPTIONS /hello.

Steps to reproduce

  1. Run the program to start the server.
  2. Exercise it via curl(see commands above).

Working code to debug

package main

import (
	"errors"
	"net/http"

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

func main() {
	e := echo.New()
	e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
		AllowOrigins: []string{"*"},
		AllowMethods: []string{http.MethodPut},
	}))
	e.GET("/hello", func(c echo.Context) error {
		return c.String(http.StatusOK, "Hello, World!")
	})
	e.OPTIONS("/hello", func(c echo.Context) error {
		c.Response().Header().Set("Allow", "GET, OPTIONS")
		c.Response().WriteHeader(http.StatusNoContent)
		return nil
	})
	if err := e.Start(":8080"); err != nil && !errors.Is(err, http.ErrServerClosed) {
		e.Logger.Fatal(err)
	}
}

Version/commit

Echo v4.11.2. See my go.mod below:

module whatever

go 1.21.3

require github.com/labstack/echo/v4 v4.11.2

require (
	github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
	github.com/labstack/gommon v0.4.0 // indirect
	github.com/mattn/go-colorable v0.1.13 // indirect
	github.com/mattn/go-isatty v0.0.19 // indirect
	github.com/valyala/bytebufferpool v1.0.0 // indirect
	github.com/valyala/fasttemplate v1.2.2 // indirect
	golang.org/x/crypto v0.14.0 // indirect
	golang.org/x/net v0.17.0 // indirect
	golang.org/x/sys v0.13.0 // indirect
	golang.org/x/text v0.13.0 // indirect
	golang.org/x/time v0.3.0 // indirect
)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions