7. The Lifecycle of a Request
Between the arrival of a request and the execution of the action, several types of objects are involved, each with a specific role. We have already encountered some of them: the static file handler, [LocaleResolver] (Chapter 5), the filter from Example 23, the argument resolvers ([HandlerMethodArgumentResolver], Example 12), and [@ControllerAdvice] (Example 15). It’s time to put them in context relative to one another.
There are two layers. The first consists of the server ([Tomcat]) and the Servlet standard ([Java]): a servlet is an object that processes requests (HTTP), and servlet filters can wrap around it to act before and after it. The second layer is [Spring MVC] itself, which is simply a servlet, [DispatcherServlet]: it selects the action, binds its parameters, executes it, and then renders the view. Within it, interceptors can act before and after the action. Whereas [ASP.NET Core] has middleware and filters (MVC), [Spring] has servlet filters and interceptors. The case study uses most of these objects:

Object | Role | In the case study |
Servlet filter | Prepares the request; does not know the action | [Spring Security] filters: reads the token; access denied without a connection |
Interceptor | code before and after the action; knows the action | (not used: roles are verified by [@PreAuthorize], see Chapter 8, and [CommonModel] completes the model) |
argument resolver | provides the value of an action parameter | those of [Spring MVC]: forms, validation, [BindingResult] |
[@ExceptionHandler] | Convert an exception into a response | invalid session: return to the login page |
[/error] error page | Generate an error response page | [ErrorController]: Pages 400, 403, 404, 500 |
7.1. Example 24: Steps in Processing a Request
Example 24 has a function that calculates the square of a number, and one object of each type. Each one writes a line to the console: this shows the order in which they are executed. The numbers listed correspond to those in the diagram.
| 24-lifecycle/
├── src/main/java/example24/
│ ├── config/
│ │ └── WebConfig.java
│ ├── controllers/
│ │ ├── CalcController.java
│ │ └── ErrorPageController.java
│ ├── filters/
│ │ ├── Block.java
│ │ ├── BlockInterceptor.java
│ │ ├── Timing.java
│ │ ├── TimingInterceptor.java
│ │ ├── IntegerParam.java
│ │ ├── IntegerParamResolver.java
│ │ ├── LoggerFilter.java
│ │ └── ErrorPageAdvice.java
│ └── Example24Application.java
├── src/main/resources/
│ ├── templates/
│ │ ├── calc/
│ │ │ └── result.html
│ │ ├── error.html
│ │ └── layout.html
│ └── application.properties
└── pom.xml
|
The files [pom.xml], [application.properties], and [Example24Application.java] are from the previous examples.
7.1.1. The servlet filter
The file [filters/LoggerFilter.java]:
| package example24.filters;
import java.io.IOException;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
// a servlet FILTER: it sees each request go by BEFORE Spring MVC, and the
// response AFTER. @Component: Spring Boot adds it to the filter chain.
@Component
public class LoggerFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain next)
throws ServletException, IOException {
String requestLine = request.getMethod() + " " + request.getRequestURI()
+ (request.getQueryString() == null ? "" : "?" + request.getQueryString());
System.out.println();
System.out.println("--- " + requestLine);
System.out.println("1. filter (before)");
next.doFilter(request, response); // the whole rest: Spring MVC, action, view
System.out.println("7. filter (after: code " + response.getStatus() + ")");
}
}
|
- lines 15–16: a filter, like the one in Example 23. [OncePerRequestFilter] is a subclass of [Spring] that implements the [Filter] interface of the Servlet standard, ensuring a single execution per request; we override its [doFilterInternal] method, which is called for each request. Using [@Component], [Spring Boot] adds the filter to the server’s filter chain;
- Lines 21–22: the method, path, and request parameters. The filter knows nothing about the action that will process the request: it intervenes before [Spring MVC], which has not yet selected it;
- line 26: the filter passes the request to the rest of the chain (the other filters, then [DispatcherServlet]) and waits for it to finish. If it did not call it, the request would stop there (it would then have to generate the response itself). The code placed before this call executes on the way out; the code placed after it executes on the way back: the filter “wraps” everything else;
- line 27: on the return path, the filter sees the response status code.
7.1.2. Interceptors
An interceptor is applied to all actions in the application (see [WebConfig] below). To ensure it acts only on certain actions, we mark those actions with a custom annotation that the interceptor will look for. The file [filters/Block.java]:
| package example24.filters;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// an annotation that marks a controller; BlockInterceptor looks for it
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Block {
}
|
- line 11: [@interface] declares an annotation. This annotation has no parameters: its mere presence is sufficient;
- line 9: [@Target] specifies where the annotation can be applied: in this case, on a type (a class);
- line 10: [@Retention(RetentionPolicy.RUNTIME)]: the annotation is preserved in the compiled code and remains readable during execution. Without this, the compiler would remove it, and the interceptor would not be able to find it.
The file [filters/Timing.java] is identical, except for the name. The file [filters/BlockInterceptor.java]:
| package example24.filters;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
// an INTERCEPTOR: it runs inside Spring MVC, once the action is chosen.
// This one decides whether the action can be executed.
public class BlockInterceptor implements HandlerInterceptor {
// preHandle: before the action; returning false stops the processing there
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// handler: what will handle the request; for an action, a HandlerMethod.
// Unlike the filter, the interceptor therefore knows the targeted action.
if (handler instanceof HandlerMethod action && action.getBeanType().isAnnotationPresent(Block.class)) {
System.out.println("2. Block interceptor (targeted action: " + action.getMethod().getName() + ")");
if (request.getParameter("block") != null) {
// a 403 error response (access denied), and nothing else
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return false; // neither binding nor action
}
}
return true;
}
}
|
- line 11: an interceptor implements the [HandlerInterceptor] interface from [Spring MVC]. Its three methods—[preHandle], [postHandle], and [afterCompletion]—have a default implementation that does nothing: we only override the ones we need;
- line 15: [preHandle] executes before the action, and even before its parameters are bound. Its parameter, [handler], is what will process the request: for an action, a [HandlerMethod] object, which describes the controller’s method;
- line 19: Unlike the filter, the interceptor therefore knows which action it is targeting. [handler instanceof HandlerMethod action] checks the object’s type and, if it matches, stores it in the variable [action], which is of the correct type. [getBeanType()] specifies the controller’s class; [isAnnotationPresent(Block.class)] indicates whether it carries the annotation [@Block];
- lines 21–25: an interceptor rejects the request by returning [false]: the rest of the process (binding, action, view) is not executed. It must then generate the response itself: in this case, [sendError(403)] creates an “access denied” error response, for which the server will generate the page (we’ll see how);
- Line 27: [true]: the request continues.
The file [filters/TimingInterceptor.java]:
| package example24.filters;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
// a second interceptor: it measures the duration of the processing
public class TimingInterceptor implements HandlerInterceptor {
private static final String KEY = "timing";
private static boolean concerned(Object handler) {
return handler instanceof HandlerMethod action && action.getBeanType().isAnnotationPresent(Timing.class);
}
// before the action (and before the binding of its parameters)
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (concerned(handler)) {
System.out.println("3. Timing interceptor (before)");
// the start time, stored in a request attribute (request scope)
request.setAttribute(KEY, System.nanoTime());
}
return true;
}
// after the action, BEFORE the view; not called if the action threw an exception
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView mav) {
if (concerned(handler)) {
System.out.println("6a. Timing interceptor (after the action; chosen view: "
+ (mav == null ? null : mav.getViewName()) + ")");
}
}
// at the very end, AFTER the view; always called, with the possible exception
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception exception) {
if (concerned(handler) && request.getAttribute(KEY) instanceof Long start) {
long ms = (System.nanoTime() - start) / 1_000_000;
String outcome = exception == null ? "" : ", exception " + exception.getClass().getSimpleName();
System.out.println("6b. Timing interceptor (end, " + ms + " ms" + outcome + ")");
}
}
}
|
- lines 15–17: the interceptor is only interested in the actions of controllers annotated with [@Timing];
- Lines 21–28: Before the action, the start time ([System.nanoTime()], in nanoseconds) is recorded and stored in a query attribute (query scope; see Chapter 6);
- lines 32–38: [postHandle] runs after the action and before the view. It receives the [ModelAndView] produced by the action (the view name and the model), and may still modify it. It is not called if the action raised an exception;
- lines 42–49: [afterCompletion] runs at the very end, after the view, regardless of whether everything went well or not: this is the right place to free a resource or measure execution time. Its last parameter is the exception, if any. Line 44: [instanceof Long start] checks and converts the attribute value in a single step.
7.1.3. Parameter Binding
The file [filters/IntegerParam.java]:
| package example24.filters;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// an integer parameter, read from the path by IntegerParamResolver (see example 12)
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface IntegerParam {
}
|
An annotation, this time on a parameter (line 9). The file [filters/IntegerParamResolver.java]:
| package example24.filters;
import java.util.Map;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.HandlerMapping;
// the binding of an @IntegerParam Integer parameter: the path variable of the same name,
// converted to an integer, or null if it is not an integer
public class IntegerParamResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(IntegerParam.class) && parameter.getParameterType() == Integer.class;
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest request, WebDataBinderFactory binderFactory) {
// the path variables ({n}...), stored by Spring in a request attribute
@SuppressWarnings("unchecked")
Map<String, String> variables = (Map<String, String>) request.getAttribute(
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
String value = variables == null ? null : variables.get(parameter.getParameterName());
System.out.println("4. parameter binding (received value: \"" + value + "\")");
try {
return Integer.valueOf(value);
} catch (NumberFormatException e) {
return null; // the action will decide
}
}
}
|
An argument resolver, like the one in Example 12:
- lines 18–20: it handles parameters of type [Integer] annotated as [@IntegerParam];
- lines 27–28: [Spring MVC] has already parsed the path ([/square/{n}]) when selecting the action; it has stored the path variables in a request attribute, in the form of a dictionary [name -> value];
- line 29: the value of the variable named after the parameter ([n]);
- lines 31–35: the value converted to an integer, or [null] if it is not an integer. An invalid value does not stop anything: the action decides.
7.1.4. Exception Handling
The file [filters/ErrorPageAdvice.java]:
| package example24.filters;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;
// @ControllerAdvice + @ExceptionHandler: turn an exception, thrown by
// any action, into a response (here, for the whole application)
@ControllerAdvice
public class ErrorPageAdvice {
// the IllegalStateException ones only: the other exceptions go their way
@ExceptionHandler(IllegalStateException.class)
public ModelAndView error(IllegalStateException e) {
System.out.println("x. @ExceptionHandler (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")");
// instead of the default error page, our [error] view, with the 500 code
ModelAndView mav = new ModelAndView("error", HttpStatus.INTERNAL_SERVER_ERROR);
mav.addObject("code", 500);
return mav;
}
}
|
- line 10: [@ControllerAdvice] makes this class a complement to all controllers (see Example 15);
- lines 14–15: a [@ExceptionHandler] method is called when an action throws an exception of the specified type, in this case [IllegalStateException] (or a derived type). If placed in a controller, it would apply only to that controller’s actions; in a [@ControllerAdvice], it applies to the entire application. It receives the exception as a parameter and generates the response just as an action would;
- lines 18–20: instead of the default error page, the view [error], with status code 500. A [ModelAndView] combines what an action usually returns in two parts: the view name and the model (and here, the status code).
The other exceptions are not covered here: we’ll see what happens to them later.
7.1.5. Configuration
The [config/WebConfig.java] file:
| package example24.config;
import java.util.List;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import example24.filters.BlockInterceptor;
import example24.filters.TimingInterceptor;
import example24.filters.IntegerParamResolver;
// the configuration of Spring MVC, complemented
@Configuration
public class WebConfig implements WebMvcConfigurer {
// Bootstrap, shared by the examples: the examples/common/bootstrap folder
// is served under the URL /bootstrap/...
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/bootstrap/**")
// "file:": a folder of the disk, relative to the current folder. Two
// attempts: started from the folder of the example, or from [examples]
.addResourceLocations("file:../common/bootstrap/", "file:common/bootstrap/");
}
// the interceptors, run in the order in which they are added
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new BlockInterceptor());
registry.addInterceptor(new TimingInterceptor());
}
// the binding of the @IntegerParam parameters
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new IntegerParamResolver());
}
}
|
- lines 30–34: the interceptors are added to [Spring MVC], which executes them in the order they were added (and [postHandle], [afterCompletion] in reverse order). They apply to all actions; they could be limited to certain paths ([.addPathPatterns("/square/**")]), but our annotations are more precise;
- lines 37–40: the argument resolver, as in Example 12.
7.1.6. The controller and the error page
The file [controllers/CalcController.java]:
| package example24.controllers;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.server.ResponseStatusException;
import example24.filters.Block;
import example24.filters.Timing;
import example24.filters.IntegerParam;
// two annotations set on the controller: the interceptors look for them
@Block
@Timing
@Controller
public class CalcController {
@GetMapping("/square/{n}")
public String square(@IntegerParam Integer n, Model model) {
System.out.println("5. action square(" + n + ")");
if (n == null) {
// a 400 error: see ErrorPageController
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
if (n == 13) {
throw new IllegalStateException("13 brings bad luck"); // an "unexpected" error
}
model.addAttribute("n", n);
model.addAttribute("square", n * n);
return "calc/result";
}
}
|
- lines 14–15: the two annotations that the interceptors are looking for;
- line 20: the [n] parameter, provided by our resolver;
- lines 22–25: an invalid parameter: a 400 error (invalid request). The [ResponseStatusException] (see Example 21) is transformed by [Spring MVC] into an error response with no content;
- lines 26–28: an “unexpected” error, to see [@ExceptionHandler] in action;
- lines 29–31: the normal case. The file [templates/calc/result.html] displays the result:
| <!doctype html>
<html xmlns:th="http://www.thymeleaf.org" th:replace="~{layout :: page(~{::main})}">
<body>
<main>
<h1 class="h4" th:text="|${n} squared = ${square}|">7 squared = 49</h1>
</main>
</body>
</html>
|
- Line 2: The page uses the [templates/layout.html] template, as expected (see Example 15): a title and [Bootstrap];
- Line 5: Between two vertical bars, [|...|] is a string in which [Thymeleaf] replaces each occurrence of [${...}] with its value.
That leaves the error page. When a request results in an error response (a call to [sendError], a [ResponseStatusException], an unknown address, or an unhandled exception), the server does not return an empty response: it forwards the request to the address [/error] to generate the page to be returned, and in the process stores the error code in a request attribute. [Spring Boot] already provides an action for this address: it is the one that has so far generated the error responses JSON (for [curl]) and the “Whitelabel Error Page” (for a browser). The file [controllers/ErrorPageController.java] replaces it:
| package example24.controllers;
import org.springframework.boot.webmvc.error.ErrorController;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
// the error page of the application. An error response (sendError, unhandled
// exception) is forwarded by the server to the /error address; implementing
// ErrorController replaces the error page of Spring Boot with ours.
@Controller
public class ErrorPageController implements ErrorController {
// @RequestMapping without an HTTP method: all the methods (GET, POST...)
@RequestMapping("/error")
public String error(HttpServletRequest request, HttpServletResponse response, Model model) {
// the original error code, stored by the server in a request attribute
Object code = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
int status = code instanceof Integer c ? c : 500;
System.out.println("8. error page " + status + " (request forwarded to /error)");
response.setStatus(status); // the page keeps the error code
model.addAttribute("code", status);
return "error";
}
}
|
- lines 15–16: a controller that implements the [ErrorController] interface from [Spring Boot]. This interface has no methods; it serves as a marker. As soon as a bean implements it, [Spring Boot] removes its own error handler;
- line 19: [@RequestMapping], which has no methods (HTTP), accepts all methods: a POST request forwarded to [/error] remains a POST request;
- lines 22–23: the original error code, stored by the server in the [RequestDispatcher.ERROR_STATUS_CODE] attribute. [code instanceof Integer c ? c : 500]: if the attribute is an integer, its value; otherwise, 500 (for example, if [/error] is requested directly);
- Lines 25–27: The page retains the error code, which is passed to the view.
The file [templates/error.html]:
| <!doctype html>
<!--/* the error page; its model: the error code */-->
<html xmlns:th="http://www.thymeleaf.org" th:replace="~{layout :: page(~{::main})}">
<body>
<main>
<div class="alert alert-danger">
<h1 class="h4" th:text="|Error ${code}|">Error 500</h1>
<!--/* th:switch / th:case: the first case equal to the value; *: all the others */-->
<p class="mb-0" th:switch="${code}">
<span th:case="400">Bad request.</span>
<span th:case="403">Access denied.</span>
<span th:case="404">Page not found.</span>
<span th:case="*">Internal server error.</span>
</p>
</div>
</main>
</body>
</html>
|
- lines 9–14: [th:switch] and [th:case]: only the first tag, [th:case], is retained, and its value is equal to that of [th:switch]; [*] corresponds to everything else. The view is used for both [ErrorPageController] and [ErrorPageAdvice].
7.1.7. Execution
Five requests: a normal calculation (http://localhost:5000/square/7), an invalid parameter ([/square/abc]), a request blocked by the interceptor ([/square/7?block=1]), an unexpected error ([/square/13]), and an unknown address ([/unknown]). The server console:
| --- GET /square/7
1. filter (before)
2. Block interceptor (targeted action: square)
3. Timing interceptor (before)
4. parameter binding (received value: "7")
5. action square(7)
6a. Timing interceptor (after the action; chosen view: calc/result)
6b. Timing interceptor (end, 5 ms)
7. filter (after: code 200)
--- GET /square/abc
1. filter (before)
2. Block interceptor (targeted action: square)
3. Timing interceptor (before)
4. parameter binding (received value: "abc")
5. action square(null)
2026-09-25T18:48:16.146+02:00 WARN 23271 --- [nio-5000-exec-3] .w.s.m.a.ResponseStatusExceptionResolver : Resolved [org.springframework.web.server.ResponseStatusException: 400 BAD_REQUEST]
6b. Timing interceptor (end, 1 ms)
7. filter (after: code 400)
8. error page 400 (request forwarded to /error)
--- GET /square/7?block=1
1. filter (before)
2. Block interceptor (targeted action: square)
7. filter (after: code 403)
8. error page 403 (request forwarded to /error)
--- GET /square/13
1. filter (before)
2. Block interceptor (targeted action: square)
3. Timing interceptor (before)
4. parameter binding (received value: "13")
5. action square(13)
x. @ExceptionHandler (IllegalStateException: 13 brings bad luck)
2026-09-25T18:48:16.174+02:00 WARN 23271 --- [nio-5000-exec-5] .m.m.a.ExceptionHandlerExceptionResolver : Resolved [java.lang.IllegalStateException: 13 brings bad luck]
6b. Timing interceptor (end, 5 ms)
7. filter (after: code 500)
--- GET /unknown
1. filter (before)
7. filter (after: code 404)
8. error page 404 (request forwarded to /error)
|
This log shows:
- the order of steps in the normal case: the filter, the interceptors ([preHandle]) in the order they were added, parameter binding, the action, [postHandle] (the view has not yet been rendered), [afterCompletion] (the view is rendered), and then the filter again, which sees the final response code;
- the interceptor that rejects the request ([/square/7?block=1]) bypasses everything else in [Spring MVC]: no second interceptor, no binding, no action;
- An invalid parameter does not prevent the action from executing: the action itself decides ([square(null)]), in this case by raising a [ResponseStatusException]. [Spring MVC] passes any exception thrown by an action to a series of exception handlers ([HandlerExceptionResolver]); the one in [ResponseStatusException] converts it into a 400 error response and logs it (line [WARN ... Resolved]). Since the action threw an exception, [postHandle] is not called; [afterCompletion] is called, without an exception: the exception has already been handled;
- the exception from [/square/13] is handled in the same way, by the resolver, which searches for the [@ExceptionHandler] methods (step x). This method generates a complete page with a 500 status code: the request ends there, without going through [/error];
- an error response with no content (400, 403, 404) is converted into a page by passing it to [/error] (step 8). Note that this step occurs after the request returns to the filter (step 7): [sendError] merely marks the response as an error, and it is the server ([Tomcat]) that, once the filter chain is complete, forwards the request to [/error]. This second pass does not go through our filter: [Spring Boot] does indeed register the filter for requests forwarded to [/error], but [OncePerRequestFilter] lets them pass without executing it (this is its default behavior, which could be changed by redefining its method [shouldNotFilterErrorDispatch]). However, it does pass through the interceptors, since it is [Spring MVC] that executes the action [/error]; but [ErrorPageController] does not carry their annotations, and they do not write anything;
- For an unknown address, no action matches: [Spring MVC] then passes the request to the static file handler (a [ResourceHttpRequestHandler]), which finds no file and throws a [NoResourceFoundException] exception, which is converted by an exception resolver into a 404 error response (with no [WARN] line in the log this time). The interceptors were indeed called, but the [handler] they received is not an action ([HandlerMethod]): they did not write anything.
The resulting pages:




Let's check that the error code is preserved:
| curl -i "http://localhost:5000/square/7?block=1"
|
| HTTP/1.1 403
Content-Type: text/html;charset=UTF-8
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Lifecycle</title>
<link rel="stylesheet" href="/bootstrap/css/bootstrap.min.css">
</head>
<body>
<div class="container py-3">
<main>
<div class="alert alert-danger">
<h1 class="h4">Error 403</h1>
<p class="mb-0">
<span>Access denied.</span>
</p>
</div>
</main>
</div>
</body>
</html>
|
Only the [span] from the case selected by [th:switch] appears on the page.
The case study combines these mechanisms: a [ErrorController] handler for pages 400, 403, 404, and 500, to which the server forwards all error responses, including those from exceptions that no one has handled (the 500 page, without the call stack); and a [@ExceptionHandler] ([web/HtmlExceptionHandler.java]) for a specific case: an invalid session (the account was deleted after login) does not display an error page but redirects to the login page.