skip to content

Inside a `WebFilter`, how do you continue the chain versus short-circuit and return a response immediately (e.g. reject a request)?

level: middleimportance: must knowfreq 60%

answer

  1. return chain.filter(exchange) = continue
  2. don't call chain = short-circuit
  3. setStatusCode + setComplete()
  4. before: .then(chain.filter); after: .doFinally
  5. Mono.empty() by mistake = dropped request

basics

~10 s

To continue, return chain.filter(exchange). To short-circuit, do NOT call the chain: set the response status/headers on exchange.getResponse() and return exchange.getResponse().setComplete() (a Mono<Void> that finishes the exchange), so no downstream filter or handler runs.

solid answer

~40 s

The `filter` method returns a `Mono<Void>` representing the whole downstream pipeline. **Continue** by returning `chain.filter(exchange)` — that invokes the next `WebFilter` and eventually the handler. **Short-circuit** by returning a `Mono<Void>` that completes *without* calling the chain: typically you set a status via `exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED)`, add headers, optionally write a body with `writeWith(...)`, and return `exchange.getResponse().setComplete()`. Because you never call `chain.filter`, the handler and later filters are skipped. You can also run work *before* continuing (`return doSomething().then(chain.filter(exchange))`) or *after* (`return chain.filter(exchange).then(after())`, or `.doFinally(...)`). The key reactive discipline: compose everything onto the returned Mono rather than executing imperatively; forgetting to return the chain silently drops the request.

code

java · 18 lines
java
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.server.*;
import reactor.core.publisher.Mono;

public class ApiKeyFilter implements WebFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        String key = exchange.getRequest().getHeaders().getFirst("X-Api-Key");
        if (key == null || key.isBlank()) {
            ServerHttpResponse response = exchange.getResponse();
            response.setStatusCode(HttpStatus.UNAUTHORIZED);        // set BEFORE commit
            return response.setComplete();                          // short-circuit
        }
        return chain.filter(exchange);                             // continue
    }
}

go deeper

for a junior

Know that returning the chain continues and not returning it stops processing.

for a middle

Show setStatusCode + setComplete() for rejection and .then()/.doFinally for before/after work.

for a senior

Discuss committed-response constraints and choosing setComplete/writeWith vs Mono.error for centralized handling.

for a principal

Reason about writeWith backpressure, DataBuffer lifecycle, and when a filter should defer to WebExceptionHandler for uniform error contracts.

## The mental model The returned `Mono<Void>` *is* the request's completion signal. Whatever you return, that is what the server subscribes to and waits on. So control flow is expressed by **what Mono you return**, not by imperative statements. ### 1. Continue to the next filter/handler ```java return chain.filter(exchange); ``` `WebFilterChain.filter` returns a `Mono<Void>` covering the rest of the pipeline. Returning it wires your filter into that flow. ### 2. Do work *before* continuing Compose upstream work, then continue: ```java return validateToken(exchange) // Mono<Void> or Mono<T> .then(chain.filter(exchange)); // run the chain only after validation completes ``` Use `.then(...)` (ignore the upstream value) or `.flatMap(v -> chain.filter(exchange))` when you need the value. ### 3. Do work *after* the handler completes ```java return chain.filter(exchange) .then(Mono.defer(() -> auditAfter(exchange))); // or for cleanup regardless of success/error: return chain.filter(exchange).doFinally(signal -> cleanup()); ``` `doFinally` runs on completion, error, or cancellation — good for closing spans / clearing MDC-like state. ### 4. Short-circuit (reject / respond without hitting the handler) Simply **don't call the chain**. Build the response and complete: ```java ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.UNAUTHORIZED); response.getHeaders().set("WWW-Authenticate", "Bearer"); return response.setComplete(); // Mono<Void> that finishes the exchange ``` Because you return this instead of `chain.filter(exchange)`, downstream filters and the handler are skipped. To also write a body: ```java byte[] bytes = "{\"error\":\"unauthorized\"}".getBytes(StandardCharsets.UTF_8); DataBuffer buffer = response.bufferFactory().wrap(bytes); response.setStatusCode(HttpStatus.UNAUTHORIZED); response.getHeaders().setContentType(MediaType.APPLICATION_JSON); return response.writeWith(Mono.just(buffer)); // writeWith returns Mono<Void> ``` ### 5. Conditional short-circuit ```java if (!hasApiKey(exchange)) { exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN); return exchange.getResponse().setComplete(); } return chain.filter(exchange); ``` ## Errors vs short-circuit Returning `Mono.error(new ResponseStatusException(HttpStatus.UNAUTHORIZED))` is another way to stop processing — it propagates to WebFlux's error handling (`WebExceptionHandler`) which renders a response. Choose this when you want centralized error rendering; choose `setComplete()`/`writeWith` when you want to write the exact response yourself. ## Gotchas - **Committed response:** once the response is committed (headers flushed) you can't change the status. Set status/headers *before* writing the body or before the handler commits. - **Dropped request:** returning `Mono.empty()` (or forgetting to return the chain) completes the exchange with no response written and no handler — a subtle bug. - **Don't block:** building the rejection must stay non-blocking; no synchronous DB lookups on the event loop. - **`setComplete()` returns a Mono** — you must *return* it, not call-and-discard. ## When to use which - Continue: the normal case, plus before/after cross-cutting work. - Short-circuit with `setComplete`/`writeWith`: auth gates, rate-limit rejections, maintenance mode, CORS preflight replies. - `Mono.error(...)`: when you prefer the framework's centralized exception handling to format the body.

  • What is the difference between returning `Mono.empty()` and returning `exchange.getResponse().setComplete()`?
    `setComplete()` finalizes the HTTP response (flushes status/headers) and completes — the client gets a proper response. Returning `Mono.empty()` completes the filter without calling the chain or committing a response, so the handler is skipped and often nothing sensible is written — usually a bug.
  • How would you instead reject the request by throwing an error, and when is that better?
    Return `Mono.error(new ResponseStatusException(HttpStatus.UNAUTHORIZED, "..."))`. It's better when you want WebFlux's `WebExceptionHandler` to render a consistent error body/format centrally, rather than writing the response yourself in each filter.

saying these in an interview costs you the question

  • Calling exchange.getResponse().setComplete() but not returning it
  • Setting the status code after the response is already committed
  • Blocking to make the reject decision (synchronous DB/HTTP call on the event loop)
  • Assuming you 'return early' with a bare return like in a servlet filter

context