Inside a `WebFilter`, how do you continue the chain versus short-circuit and return a response immediately (e.g. reject a request)?
answer
- return chain.filter(exchange) = continue
- don't call chain = short-circuit
- setStatusCode + setComplete()
- before: .then(chain.filter); after: .doFinally
- Mono.empty() by mistake = dropped request
basics
~10 sTo 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 sThe `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 linesimport 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
Know that returning the chain continues and not returning it stops processing.
Show setStatusCode + setComplete() for rejection and .then()/.doFinally for before/after work.
Discuss committed-response constraints and choosing setComplete/writeWith vs Mono.error for centralized handling.
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