A service method guarded by `@PreAuthorize` used to return 403 to unauthorised callers and 401 to anonymous ones. After someone added a `@RestControllerAdvice` with `@ExceptionHandler(Exception.class)` that returns a generic 500 body, those calls now return 500. The exception is thrown long after the security filters have "run". How did it ever become a 403, and what changed?
The security filters have not finished when the controller runs; they are still on the call stack. FilterChainProxy does not loop over its filters, it nests them: each filter calls chain.doFilter() to reach the next, and DispatcherServlet runs at the very bottom, inside every filter's call. ExceptionTranslationFilter wraps its call in a try/catch, so an AccessDeniedException thrown from a method-security proxy inside the controller call propagates up through the dispatcher and is caught there and turned into 401 or 403. The new advice sits below that point: DispatcherServlet offers exceptions to @ExceptionHandler methods first, the catch-all handled AccessDeniedException as a generic error, and the exception never reached the filter. Let security exceptions through the advice (rethrow them, or handle them explicitly with the right status).