# 3. The three identical 403s *Prev: [2. Who resolves the CorsConfigurationSource](02-who-resolves-the-source.md) · Next: [4. Preflight handlers](04-preflight-handlers.md)* `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: ```yaml 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](02-who-resolves-the-source.md) · Next: [4. Preflight handlers](04-preflight-handlers.md)*