skip to content

What is a HandlerExecutionChain, and how do the HandlerInterceptor callbacks it carries execute around the handler — including when a request is short-circuited?

level: seniorimportance: should knowfreq 40%

answer

  1. chain = handler + matched interceptors
  2. preHandle forward; postHandle & afterCompletion reverse
  3. preHandle false → short-circuit, handler skipped
  4. afterCompletion only for succeeded preHandles, gets Exception
  5. postHandle skipped on exception; before view render

basics

~20 s

A HandlerExecutionChain pairs the chosen handler with the interceptors that apply to the request. The DispatcherServlet runs each interceptor's preHandle before the handler, postHandle after it (before view rendering), and afterCompletion at the very end. If a preHandle returns false, the request is stopped early.

solid answer

~40 s

HandlerMapping.getHandler returns a HandlerExecutionChain wrapping the handler plus the ordered HandlerInterceptors whose path patterns match. DispatcherServlet.doDispatch drives them: it calls applyPreHandle, invoking each interceptor's preHandle in registration order; if any returns false, dispatch stops immediately and afterCompletion runs for the already-successful interceptors (the handler never executes). If all preHandles return true, the HandlerAdapter invokes the handler, then postHandle runs in reverse order (before view rendering, with access to the ModelAndView), the view renders, and finally afterCompletion runs in reverse order for cleanup — it fires even if the handler or view threw, making it the place for resource release and error logging.

code

java · 25 lines
java
public class TimingInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) {
        req.setAttribute("start", System.nanoTime());
        return true; // false here would skip the handler AND its own afterCompletion
    }
    @Override
    public void postHandle(HttpServletRequest req, HttpServletResponse resp,
                           Object handler, ModelAndView mav) {
        // runs before view render, ONLY if handler didn't throw
    }
    @Override
    public void afterCompletion(HttpServletRequest req, HttpServletResponse resp,
                                Object handler, Exception ex) {
        long ms = (System.nanoTime() - (long) req.getAttribute("start")) / 1_000_000;
        // runs even if ex != null — the right place for cleanup/metrics
    }
}

@Configuration
class WebConfig implements WebMvcConfigurer {
    @Override public void addInterceptors(InterceptorRegistry reg) {
        reg.addInterceptor(new TimingInterceptor()).addPathPatterns("/api/**");
    }
}

go deeper

for a junior

Know the chain carries interceptors and preHandle runs before, postHandle/afterCompletion after the handler.

for a middle

Explain the forward/reverse ordering and that preHandle returning false short-circuits.

for a senior

Detail the interceptorIndex short-circuit semantics and why afterCompletion is the cleanup hook.

for a principal

Contrast interceptors vs filters vs @ControllerAdvice, and reason about MappedInterceptor path filtering and REST response-already-committed pitfalls.

## What the chain is A `HandlerExecutionChain` (`org.springframework.web.servlet.HandlerExecutionChain`) is the object `getHandler` returns. It holds: - the **handler** (e.g. a `HandlerMethod`), and - an ordered `HandlerInterceptor[]` — the interceptors whose URL path patterns match this request (path inclusion/exclusion is applied via `MappedInterceptor` when the registry is built). Interceptors are the Spring MVC analog of a servlet `Filter` but *handler-aware*: they run inside the DispatcherServlet, after handler selection, and can see the chosen `HandlerMethod` and the resulting `ModelAndView`. ## The three callbacks `HandlerInterceptor` defines three default methods: 1. `boolean preHandle(request, response, handler)` — runs **before** the handler. Returning `false` short-circuits: no handler, no further interceptors. 2. `void postHandle(request, response, handler, ModelAndView)` — runs **after** the handler but **before view rendering**; can mutate the model/view. Not called if the handler threw. 3. `void afterCompletion(request, response, handler, Exception)` — runs **after** rendering completes (or after an exception), for cleanup. The `Exception` param is non-null if one occurred. ## Exact orchestration in doDispatch ``` mappedHandler = getHandler(request); // the chain if (!mappedHandler.applyPreHandle(req, resp)) // preHandle in forward order return; // short-circuit mav = handlerAdapter.handle(...); // invoke handler mappedHandler.applyPostHandle(req, resp, mav); // postHandle in REVERSE order processDispatchResult(...); // render view // finally: triggerAfterCompletion(...) in REVERSE order ``` - **preHandle order**: registration/forward order (index 0 → n). - **postHandle & afterCompletion order**: **reverse** (n → 0), mirroring a nested-scope wrap. ## Short-circuit semantics (the subtle part) If interceptor #2's `preHandle` returns `false`: - The handler is **not** invoked. - `postHandle` runs for **nobody**. - `afterCompletion` runs **only for interceptors whose preHandle already returned true** (i.e. #1, #0), in reverse — DispatcherServlet tracks an `interceptorIndex` for exactly this. The interceptor that returned false must clean up itself before returning, because its own afterCompletion won't be called. ## afterCompletion always fires (on the successful prefix) Even if the handler or the view rendering throws, `afterCompletion` is invoked (with the Exception) for every interceptor whose `preHandle` succeeded. That guarantee makes it the correct place for releasing resources, closing timers/spans, or MDC cleanup — postHandle is not, because it is skipped on exceptions. ## Gotchas - `postHandle` is skipped when the handler throws — don't put critical cleanup there. - With `@ResponseBody`/REST, the response body is often already written by the time `postHandle` runs, so modifying the `ModelAndView` there has no effect on the body (it's usually null). - Interceptors are configured via `WebMvcConfigurer.addInterceptors`, which creates `MappedInterceptor`s carrying include/exclude path patterns — that path filtering is what decides membership in a given chain. - Interceptors ≠ Filters: filters wrap the whole servlet and run outside DispatcherServlet; interceptors are handler-aware and run inside it.

  • If the second of three interceptors returns false from preHandle, whose afterCompletion runs?
    Only the first interceptor's afterCompletion runs (reverse order over the succeeded prefix). The handler and postHandle are skipped entirely, and the second interceptor's own afterCompletion is NOT called — it must clean up before returning false.
  • Why is afterCompletion, not postHandle, the right place to release resources?
    postHandle is skipped whenever the handler or a prior step throws, so cleanup there can be silently missed. afterCompletion always runs for interceptors whose preHandle succeeded and receives any Exception, guaranteeing the cleanup executes.

saying these in an interview costs you the question

  • Saying postHandle runs even when the handler throws (it doesn't)
  • Thinking all three callbacks run in the same order (postHandle/afterCompletion are reversed)
  • Believing afterCompletion runs for an interceptor whose preHandle returned false
  • Confusing HandlerInterceptor with servlet Filter (filters run outside DispatcherServlet)

context