# 5. The `/error` dispatch, or why your 403 arrives as a 401 *Prev: [4. Preflight handlers](04-preflight-handlers.md) · Next: [6. CSRF for SPAs](06-csrf-for-spas.md)* Two of the failures in this module — the wildcard/credentials clash in chapter 4 and the CSRF rejection in chapter 6 — produce a status code that has nothing to do with what went wrong. Both have the same cause, and it is worth understanding once because it explains a large fraction of confusing Spring Security bug reports. ## The mechanism 1. Something inside the chain calls `response.sendError(403, ..)` (that is what `AccessDeniedHandlerImpl` does) or lets an exception escape `FilterChainProxy`. 2. The servlet container does not write that response. It **re-dispatches** the request internally to `/error`, with `DispatcherType.ERROR`. 3. Spring Boot registers `springSecurityFilterChain` for **every** dispatcher type: `SecurityFilterProperties.dispatcherTypes` defaults to `EnumSet.allOf(DispatcherType.class)`. So the whole security chain runs again on that dispatch. 4. On the second pass, the filters that extend `OncePerRequestFilter` skip themselves — `shouldNotFilterErrorDispatch()` defaults to `true`. `BasicAuthenticationFilter` is one of them. The credential is never re-read. 5. The filters that extend `GenericFilterBean` do run. `AuthorizationFilter` is one of them. 6. So the second pass is **authorized but not authenticated**: `AuthorizationFilter` evaluates `/error` against `anyRequest().authenticated()`, finds an anonymous principal, and denies it. 7. The 401 from step 6 is what reaches the client. The 403 from step 1 is gone. The mechanism is set out in full in [The Spring Security Filter Chain Explained](https://ankurm.com/spring-security-filter-chain-explained/); this chapter is what it looks like when it lands on a CORS or CSRF problem. ## Proving it in one diff The `errorpermit` profile adds one filter chain, `@Order(0)`, matching `/error` and permitting everything. Nothing else changes. ``` ./scripts/run.sh csrfnaive POST → 401, empty body, WWW-Authenticate: Basic ./scripts/run.sh csrfnaive,errorpermit POST → 403, {"status":403,"error":"Forbidden", ...} ``` [`docs/output/08-csrf-naive.txt`](output/08-csrf-naive.txt) against [`docs/output/09-error-dispatch.txt`](output/09-error-dispatch.txt). ## What to do about it Permit `/error`. It is not a hole: the error page is generated from an attribute the container set, and an unauthenticated request cannot reach it except through a dispatch the container initiated. ```java @Bean @Order(0) SecurityFilterChain errorChain(HttpSecurity http) throws Exception { return http.securityMatcher("/error") .authorizeHttpRequests((auth) -> auth.anyRequest().permitAll()) .csrf(CsrfConfigurer::disable) .build(); } ``` The alternative is to narrow the dispatcher types so the chain does not run on the error dispatch at all: ```yaml spring.security.filter.dispatcher-types: request ``` That one is broader in effect than it looks; prefer the `/error` chain unless you have a specific reason. ## Why this matters more for a SPA than for a server-rendered app A browser will not let a SPA read a cross-origin response unless the CORS headers are present. When the 403 is replaced by a 401 written on a dispatch where `CorsFilter` may or may not have re-run, what the developer sees in the console is neither "403" nor "CSRF"; it is `TypeError: Failed to fetch`. Every layer of the stack has thrown away the actual cause by then. --- *Prev: [4. Preflight handlers](04-preflight-handlers.md) · Next: [6. CSRF for SPAs](06-csrf-for-spas.md)*