3.3 KiB
3. The three identical 403s
Prev: 2. Who resolves the CorsConfigurationSource · Next: 4. Preflight handlers
DefaultCorsProcessor runs three checks on a preflight, in order: origin, method, request
headers. All three failures produce the same thing.
HTTP/1.1 403
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Invalid CORS request
Same status, same body, byte for byte, no Access-Control-* header to distinguish them. The
assertion in CorsContractTests.threeRejectionsLookIdentical compares the three bodies for
equality, so if a future version starts distinguishing them, that test fails.
The only place the difference exists is a DEBUG log line, and the three are worth memorising because they are the fastest CORS diagnosis available:
o.s.web.cors.DefaultCorsProcessor : Reject: 'https://evil.example.com' origin is not allowed
o.s.web.cors.DefaultCorsProcessor : Reject: HTTP 'DELETE' is not allowed
o.s.web.cors.DefaultCorsProcessor : Reject: headers '[authorization]' are not allowed
Turn them on with:
logging.level.org.springframework.web.cors: DEBUG
The complete set of messages, read out of the class's constant pool, is five:
| Message | Meaning |
|---|---|
Skip: no CORS configuration has been provided |
The source returned null for this path — chapter 2 |
Skip: response already contains "Access-Control-Allow-Origin" |
Something upstream already handled it |
Reject: origin is malformed |
The Origin header did not parse |
Reject: '…' origin is not allowed |
|
Reject: HTTP '…' is not allowed |
|
Reject: headers '[…]' are not allowed |
Status codes, and what each one means
Collecting the states this module reproduces:
| What you see | What it means |
|---|---|
401/403, no Access-Control-* at all |
No CorsFilter in the chain. The preflight was judged by authorization — chapter 1 |
200, no Access-Control-* |
CorsFilter is present and found no configuration for this path — chapter 2 |
403, Invalid CORS request |
CorsFilter is present and rejected origin, method or headers — this chapter |
200 with Access-Control-Allow-Origin |
It worked |
404, no Access-Control-* |
The path is outside the pattern you registered. Common with /api/** versus a mis-typed URL |
The browser reports the same "blocked by CORS policy" for the first four rows. Two of them are not CORS problems.
What a simple request does
Only preflighted requests get intercepted. A simple GET runs the whole chain, so an
unauthenticated one returns 401 — carrying the CORS header, because CorsFilter at
1000 already wrote it before AuthorizationFilter at 4200 rejected the request:
HTTP/1.1 401
Access-Control-Allow-Origin: https://spa.example.com
Access-Control-Allow-Credentials: true
That is the good case: the SPA's fetch resolves and the code can read response.status. It is
also the reason "my POST fails but my GET returns a readable 401" is a coherent bug report and
not a contradiction.
Prev: 2. Who resolves the CorsConfigurationSource · Next: 4. Preflight handlers