@@ -19,6 +19,7 @@ func TestCORS(t *testing.T) {
1919 e := echo .New ()
2020 req := httptest .NewRequest (http .MethodOptions , "/" , nil ) // Preflight request
2121 req .Header .Set (echo .HeaderOrigin , "http://example.com" )
22+ req .Header .Set (echo .HeaderAccessControlRequestMethod , http .MethodGet )
2223 rec := httptest .NewRecorder ()
2324 c := e .NewContext (req , rec )
2425
@@ -33,6 +34,79 @@ func TestCORS(t *testing.T) {
3334 assert .Equal (t , "*" , rec .Header ().Get (echo .HeaderAccessControlAllowOrigin ))
3435}
3536
37+ // TestCORS_NonPreflightOPTIONSPassThrough locks in #2534: only true CORS
38+ // preflights short-circuit the middleware. OPTIONS without the preflight
39+ // headers must reach the next handler.
40+ func TestCORS_NonPreflightOPTIONSPassThrough (t * testing.T ) {
41+ e := echo .New ()
42+ mw := CORS ("*" )
43+
44+ t .Run ("OPTIONS without Origin reaches next" , func (t * testing.T ) {
45+ req := httptest .NewRequest (http .MethodOptions , "/hello" , nil )
46+ rec := httptest .NewRecorder ()
47+ c := e .NewContext (req , rec )
48+ called := false
49+ handler := mw (func (c * echo.Context ) error {
50+ called = true
51+ return c .NoContent (http .StatusNoContent )
52+ })
53+ assert .NoError (t , handler (c ))
54+ assert .True (t , called , "expected next to run for non-preflight OPTIONS" )
55+ assert .Empty (t , rec .Header ().Get (echo .HeaderAccessControlAllowMethods ))
56+ })
57+
58+ t .Run ("OPTIONS with Origin but without Access-Control-Request-Method reaches next" , func (t * testing.T ) {
59+ req := httptest .NewRequest (http .MethodOptions , "/hello" , nil )
60+ req .Header .Set (echo .HeaderOrigin , "https://example.com" )
61+ rec := httptest .NewRecorder ()
62+ c := e .NewContext (req , rec )
63+ called := false
64+ handler := mw (func (c * echo.Context ) error {
65+ called = true
66+ return c .NoContent (http .StatusNoContent )
67+ })
68+ assert .NoError (t , handler (c ))
69+ assert .True (t , called , "expected next to run when ACR-Method is missing" )
70+ // Origin was allowed; simple CORS headers may be set, but not preflight Allow-Methods.
71+ assert .Equal (t , "*" , rec .Header ().Get (echo .HeaderAccessControlAllowOrigin ))
72+ assert .Empty (t , rec .Header ().Get (echo .HeaderAccessControlAllowMethods ))
73+ })
74+
75+ t .Run ("true preflight still short-circuits next" , func (t * testing.T ) {
76+ req := httptest .NewRequest (http .MethodOptions , "/hello" , nil )
77+ req .Header .Set (echo .HeaderOrigin , "https://example.com" )
78+ req .Header .Set (echo .HeaderAccessControlRequestMethod , http .MethodPut )
79+ rec := httptest .NewRecorder ()
80+ c := e .NewContext (req , rec )
81+ called := false
82+ handler := mw (func (c * echo.Context ) error {
83+ called = true
84+ return c .String (http .StatusOK , "should not run" )
85+ })
86+ assert .NoError (t , handler (c ))
87+ assert .False (t , called , "preflight must not call next" )
88+ assert .Equal (t , http .StatusNoContent , rec .Code )
89+ assert .Equal (t , "*" , rec .Header ().Get (echo .HeaderAccessControlAllowOrigin ))
90+ assert .NotEmpty (t , rec .Header ().Get (echo .HeaderAccessControlAllowMethods ))
91+ })
92+ }
93+
94+ func TestIsCORSPreflight (t * testing.T ) {
95+ req := httptest .NewRequest (http .MethodOptions , "/" , nil )
96+ assert .False (t , isCORSPreflight (req ))
97+
98+ req .Header .Set (echo .HeaderOrigin , "https://example.com" )
99+ assert .False (t , isCORSPreflight (req ))
100+
101+ req .Header .Set (echo .HeaderAccessControlRequestMethod , http .MethodPut )
102+ assert .True (t , isCORSPreflight (req ))
103+
104+ req = httptest .NewRequest (http .MethodGet , "/" , nil )
105+ req .Header .Set (echo .HeaderOrigin , "https://example.com" )
106+ req .Header .Set (echo .HeaderAccessControlRequestMethod , http .MethodPut )
107+ assert .False (t , isCORSPreflight (req ))
108+ }
109+
36110func TestCORSConfig (t * testing.T ) {
37111 var testCases = []struct {
38112 name string
@@ -275,6 +349,11 @@ func TestCORSConfig(t *testing.T) {
275349 for k , v := range tc .whenHeaders {
276350 req .Header .Set (k , v )
277351 }
352+ // Intentional preflight cases: OPTIONS + Origin need ACR-Method per Fetch.
353+ if method == http .MethodOptions && req .Header .Get (echo .HeaderOrigin ) != "" &&
354+ req .Header .Get (echo .HeaderAccessControlRequestMethod ) == "" {
355+ req .Header .Set (echo .HeaderAccessControlRequestMethod , http .MethodGet )
356+ }
278357
279358 err = h (c )
280359
@@ -413,6 +492,10 @@ func TestCORSWithConfig_AllowMethods(t *testing.T) {
413492 c := e .NewContext (req , rec )
414493
415494 req .Header .Set (echo .HeaderOrigin , tc .whenOrigin )
495+ if tc .whenOrigin != "" {
496+ // Real preflight requires Access-Control-Request-Method (#2534).
497+ req .Header .Set (echo .HeaderAccessControlRequestMethod , http .MethodGet )
498+ }
416499 if tc .whenAllowContextKey != "" {
417500 c .Set (echo .ContextKeyHeaderAllow , tc .whenAllowContextKey )
418501 }
@@ -479,12 +562,13 @@ func TestCorsHeaders(t *testing.T) {
479562 expectStatus : http .StatusOK ,
480563 },
481564 {
482- name : "preflight, allow any origin, missing origin header = no CORS logic done" ,
565+ // OPTIONS without Origin is not a CORS preflight; request continues to the router.
566+ name : "OPTIONS no origin, allow any origin = no CORS preflight short-circuit" ,
483567 originDomain : "" , // Request does not have Origin header
484568 allowedOrigin : "*" ,
485569 method : http .MethodOptions ,
486570 expected : false ,
487- expectStatus : http .StatusNoContent ,
571+ expectStatus : http .StatusNoContent , // router default OPTIONS/Allow path
488572 expectAllowHeader : "OPTIONS, GET, POST" ,
489573 },
490574 {
@@ -497,7 +581,7 @@ func TestCorsHeaders(t *testing.T) {
497581 expectAllowHeader : "OPTIONS, GET, POST" ,
498582 },
499583 {
500- name : "preflight, allow any origin, missing origin header = no CORS logic done " ,
584+ name : "OPTIONS no origin, allow specific origin = no CORS preflight short-circuit " ,
501585 originDomain : "" , // Request does not have Origin header
502586 allowedOrigin : "http://example.com" ,
503587 method : http .MethodOptions ,
@@ -548,6 +632,15 @@ func TestCorsHeaders(t *testing.T) {
548632 if tc .originDomain != "" {
549633 req .Header .Set (echo .HeaderOrigin , tc .originDomain )
550634 }
635+ // True CORS preflight requires Access-Control-Request-Method (#2534).
636+ if tc .method == http .MethodOptions && tc .originDomain != "" && tc .expected {
637+ req .Header .Set (echo .HeaderAccessControlRequestMethod , http .MethodGet )
638+ }
639+ // Disallowed origin + OPTIONS still needs ACR-Method to be classified as preflight
640+ // (middleware then omits ACAO and returns 204).
641+ if tc .method == http .MethodOptions && tc .originDomain != "" && ! tc .expected {
642+ req .Header .Set (echo .HeaderAccessControlRequestMethod , http .MethodGet )
643+ }
551644
552645 // we run through whole Echo handler chain to see how CORS works with Router OPTIONS handler
553646 e .ServeHTTP (rec , req )
@@ -606,6 +699,7 @@ func Test_allowOriginFunc(t *testing.T) {
606699 rec := httptest .NewRecorder ()
607700 c := e .NewContext (req , rec )
608701 req .Header .Set (echo .HeaderOrigin , origin )
702+ req .Header .Set (echo .HeaderAccessControlRequestMethod , http .MethodGet )
609703 cors , err := CORSConfig {UnsafeAllowOriginFunc : allowOriginFunc }.ToMiddleware ()
610704 assert .NoError (t , err )
611705
0 commit comments