Skip to content

Commit 4839536

Browse files
fix(middleware): only short-circuit CORS preflights
Treat OPTIONS requests as CORS preflights only when Origin and Access-Control-Request-Method are present, so non-preflight OPTIONS handlers can run. Fixes #2534 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 4f2f975 commit 4839536

3 files changed

Lines changed: 91 additions & 17 deletions

File tree

bind_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1577,7 +1577,7 @@ func TestTimeFormatBinding(t *testing.T) {
15771577
DateTimeLocal time.Time `form:"datetime_local" format:"2006-01-02T15:04"`
15781578
Date time.Time `query:"date" format:"2006-01-02"`
15791579
CustomFormat time.Time `form:"custom" format:"01/02/2006 15:04:05"`
1580-
DefaultTime time.Time `form:"default_time"` // No format tag - should use default parsing
1580+
DefaultTime time.Time `form:"default_time"` // No format tag - should use default parsing
15811581
PtrTime *time.Time `query:"ptr_time" format:"2006-01-02"`
15821582
}
15831583

@@ -1623,7 +1623,7 @@ func TestTimeFormatBinding(t *testing.T) {
16231623
{
16241624
name: "nok, wrong format should fail",
16251625
contentType: MIMEApplicationForm,
1626-
data: "datetime_local=2023-12-25", // Missing time part
1626+
data: "datetime_local=2023-12-25", // Missing time part
16271627
expectError: true,
16281628
},
16291629
}

middleware/cors.go

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -190,18 +190,12 @@ func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc {
190190

191191
res.Header().Add(echo.HeaderVary, echo.HeaderOrigin)
192192

193-
// Preflight request is an OPTIONS request, using three HTTP request headers: Access-Control-Request-Method,
194-
// Access-Control-Request-Headers, and the Origin header. See: https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request
195-
// For simplicity we just consider method type and later `Origin` header.
196-
preflight := req.Method == http.MethodOptions
197-
198-
// Although router adds special handler in case of OPTIONS method we avoid calling next for OPTIONS in this middleware
199-
// as CORS requests do not have cookies / authentication headers by default, so we could get stuck in auth
200-
// middlewares by calling next(c).
201-
// But we still want to send `Allow` header as response in case of Non-CORS OPTIONS request as router default
202-
// handler does.
193+
preflight := isCORSPreflight(req)
194+
195+
// Echo's router adds an Allow header for OPTIONS requests. Copy it before true
196+
// CORS preflight requests short-circuit the handler chain.
203197
routerAllowMethods := ""
204-
if preflight {
198+
if req.Method == http.MethodOptions {
205199
tmpAllowMethods, ok := c.Get(echo.ContextKeyHeaderAllow).(string)
206200
if ok && tmpAllowMethods != "" {
207201
routerAllowMethods = tmpAllowMethods
@@ -211,10 +205,7 @@ func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc {
211205

212206
// No Origin provided. This is (probably) not request from actual browser - proceed executing middleware chain
213207
if origin == "" {
214-
if !preflight {
215-
return next(c)
216-
}
217-
return c.NoContent(http.StatusNoContent)
208+
return next(c)
218209
}
219210

220211
if config.AllowOriginFunc != nil {
@@ -305,3 +296,9 @@ func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc {
305296
}
306297
}
307298
}
299+
300+
func isCORSPreflight(r *http.Request) bool {
301+
return r.Method == http.MethodOptions &&
302+
r.Header.Get(echo.HeaderOrigin) != "" &&
303+
r.Header.Get(echo.HeaderAccessControlRequestMethod) != ""
304+
}

middleware/cors_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,10 @@ func TestCORS(t *testing.T) {
246246
for k, v := range tc.whenHeaders {
247247
req.Header.Set(k, v)
248248
}
249+
if method == http.MethodOptions && req.Header.Get(echo.HeaderOrigin) != "" &&
250+
req.Header.Get(echo.HeaderAccessControlRequestMethod) == "" {
251+
req.Header.Set(echo.HeaderAccessControlRequestMethod, http.MethodGet)
252+
}
249253

250254
err := h(c)
251255

@@ -265,6 +269,70 @@ func TestCORS(t *testing.T) {
265269
}
266270
}
267271

272+
func TestCORS_NonPreflightOPTIONSPassThrough(t *testing.T) {
273+
e := echo.New()
274+
cors := CORSWithConfig(CORSConfig{
275+
AllowOrigins: []string{"*"},
276+
AllowMethods: []string{http.MethodPut},
277+
})
278+
279+
for _, tc := range []struct {
280+
name string
281+
headers map[string]string
282+
}{
283+
{
284+
name: "OPTIONS without Origin",
285+
},
286+
{
287+
name: "OPTIONS with Origin but without Access-Control-Request-Method",
288+
headers: map[string]string{
289+
echo.HeaderOrigin: "https://example.com",
290+
},
291+
},
292+
} {
293+
t.Run(tc.name, func(t *testing.T) {
294+
req := httptest.NewRequest(http.MethodOptions, "/hello", nil)
295+
for k, v := range tc.headers {
296+
req.Header.Set(k, v)
297+
}
298+
rec := httptest.NewRecorder()
299+
c := e.NewContext(req, rec)
300+
called := false
301+
302+
h := cors(func(c echo.Context) error {
303+
called = true
304+
c.Response().Header().Set(echo.HeaderAllow, "GET, OPTIONS")
305+
return c.NoContent(http.StatusNoContent)
306+
})
307+
308+
assert.NoError(t, h(c))
309+
assert.True(t, called)
310+
assert.Equal(t, "GET, OPTIONS", rec.Header().Get(echo.HeaderAllow))
311+
assert.Empty(t, rec.Header().Get(echo.HeaderAccessControlAllowMethods))
312+
})
313+
}
314+
315+
t.Run("true preflight short-circuits next", func(t *testing.T) {
316+
req := httptest.NewRequest(http.MethodOptions, "/hello", nil)
317+
req.Header.Set(echo.HeaderOrigin, "https://example.com")
318+
req.Header.Set(echo.HeaderAccessControlRequestMethod, http.MethodPut)
319+
rec := httptest.NewRecorder()
320+
c := e.NewContext(req, rec)
321+
called := false
322+
323+
h := cors(func(c echo.Context) error {
324+
called = true
325+
return c.NoContent(http.StatusOK)
326+
})
327+
328+
assert.NoError(t, h(c))
329+
assert.False(t, called)
330+
assert.Equal(t, http.StatusNoContent, rec.Code)
331+
assert.Equal(t, "*", rec.Header().Get(echo.HeaderAccessControlAllowOrigin))
332+
assert.Equal(t, "PUT", rec.Header().Get(echo.HeaderAccessControlAllowMethods))
333+
})
334+
}
335+
268336
func Test_allowOriginScheme(t *testing.T) {
269337
tests := []struct {
270338
domain, pattern string
@@ -298,6 +366,7 @@ func Test_allowOriginScheme(t *testing.T) {
298366
rec := httptest.NewRecorder()
299367
c := e.NewContext(req, rec)
300368
req.Header.Set(echo.HeaderOrigin, tt.domain)
369+
req.Header.Set(echo.HeaderAccessControlRequestMethod, http.MethodGet)
301370
cors := CORSWithConfig(CORSConfig{
302371
AllowOrigins: []string{tt.pattern},
303372
})
@@ -389,6 +458,7 @@ func Test_allowOriginSubdomain(t *testing.T) {
389458
rec := httptest.NewRecorder()
390459
c := e.NewContext(req, rec)
391460
req.Header.Set(echo.HeaderOrigin, tt.domain)
461+
req.Header.Set(echo.HeaderAccessControlRequestMethod, http.MethodGet)
392462
cors := CORSWithConfig(CORSConfig{
393463
AllowOrigins: []string{tt.pattern},
394464
})
@@ -472,6 +542,9 @@ func TestCORSWithConfig_AllowMethods(t *testing.T) {
472542
c := e.NewContext(req, rec)
473543

474544
req.Header.Set(echo.HeaderOrigin, tc.whenOrigin)
545+
if tc.whenOrigin != "" {
546+
req.Header.Set(echo.HeaderAccessControlRequestMethod, http.MethodGet)
547+
}
475548
if tc.allowContextKey != "" {
476549
c.Set(echo.ContextKeyHeaderAllow, tc.allowContextKey)
477550
}
@@ -605,6 +678,9 @@ func TestCorsHeaders(t *testing.T) {
605678
if tc.originDomain != "" {
606679
req.Header.Set(echo.HeaderOrigin, tc.originDomain)
607680
}
681+
if tc.method == http.MethodOptions && tc.originDomain != "" {
682+
req.Header.Set(echo.HeaderAccessControlRequestMethod, http.MethodGet)
683+
}
608684

609685
// we run through whole Echo handler chain to see how CORS works with Router OPTIONS handler
610686
e.ServeHTTP(rec, req)
@@ -663,6 +739,7 @@ func Test_allowOriginFunc(t *testing.T) {
663739
rec := httptest.NewRecorder()
664740
c := e.NewContext(req, rec)
665741
req.Header.Set(echo.HeaderOrigin, origin)
742+
req.Header.Set(echo.HeaderAccessControlRequestMethod, http.MethodGet)
666743
cors := CORSWithConfig(CORSConfig{
667744
AllowOriginFunc: allowOriginFunc,
668745
})

0 commit comments

Comments
 (0)