What is ServerWebExchange, and how does it differ from the servlet request/response model?
answer
- One object = request+response+attributes+session
- Body is Flux<DataBuffer>, not InputStream
- No ThreadLocal — state in exchange + Reactor Context
- exchange.mutate() to rewrite
- Release DataBuffers / body read-once
basics
~10 sServerWebExchange is WebFlux's per-request container. It holds the reactive request (ServerHttpRequest) and response (ServerHttpResponse) plus attributes and session. Unlike servlets, bodies are reactive streams (Flux<DataBuffer>), not blocking InputStreams.
solid answer
~40 s`ServerWebExchange` is the reactive contract for a single HTTP exchange, passed through every WebFlux stage (`WebFilter`, `WebHandler`, `DispatcherHandler`). It exposes `getRequest()` → `ServerHttpRequest` and `getResponse()` → `ServerHttpResponse`, plus `getAttributes()`, `getSession()` (a `Mono<WebSession>`), `getPrincipal()`, form/multipart data, and matched-pattern info. The crucial difference from the servlet model: the body is a reactive stream — `Flux<DataBuffer>` — read/written non-blockingly rather than via blocking `InputStream`/`OutputStream`, and there is no `ThreadLocal`-bound `HttpServletRequest`. Request/response objects are immutable-ish: you don't mutate the URI, you build a mutated exchange via `exchange.mutate()`. It's also decoupled from the servlet API entirely, so the same code runs on Netty, Undertow, or a Servlet 3.1 container. Headers are exposed as read-only `HttpHeaders` from the request.
code
java · 18 lines@RestController
class DownloadController {
// Inject the exchange for low-level access
@GetMapping("/ping")
Mono<Void> ping(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
String trace = request.getHeaders().getFirst("X-Trace-Id");
response.getHeaders().add("X-Trace-Id", trace == null ? "n/a" : trace);
response.setStatusCode(HttpStatus.ACCEPTED);
byte[] body = "pong".getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(body);
return response.writeWith(Mono.just(buffer)); // non-blocking write
}
}go deeper
Know it bundles the reactive request and response for one call.
Contrast Flux<DataBuffer> vs blocking streams and list the main accessors.
Explain no-ThreadLocal, mutate(), DataBuffer release, and single-subscription body caching.
Reason about server-agnostic abstraction, context propagation across threads, and gateway-style request rewriting.
**`ServerWebExchange`** (`org.springframework.web.server.ServerWebExchange`) is the reactive equivalent of the pairing of `HttpServletRequest`+`HttpServletResponse`+request attributes+session, unified into one object that flows through the WebFlux pipeline. Every `WebFilter.filter(exchange, chain)` and `WebHandler.handle(exchange)` receives it. **What it holds.** - `getRequest()` → **`ServerHttpRequest`**: method, URI, path, query params (`getQueryParams()`), read-only `HttpHeaders`, cookies (`getCookies()`), remote/local address, and the body as `Flux<DataBuffer>` via `getBody()`. - `getResponse()` → **`ServerHttpResponse`**: mutable status (`setStatusCode`), headers, cookies (`addCookie`), and write methods `writeWith(Publisher<DataBuffer>)` / `writeAndFlushWith(...)`, plus `beforeCommit(...)` hooks and a `bufferFactory()`. - `getAttributes()` → a mutable `Map<String,Object>` for passing data between filters/handlers (the reactive replacement for `request.setAttribute`). - `getSession()` → `Mono<WebSession>` — the session is fetched reactively, not a blocking `HttpSession`. - `getPrincipal()` → `Mono<Principal>`, `getFormData()` → `Mono<MultiValueMap<String,String>>`, `getMultipartData()`, `getLocaleContext()`, and matched-pattern/URI-template attributes. **Key differences from the servlet model.** 1. **Non-blocking body I/O.** Servlets expose blocking `getInputStream()`/`getWriter()`. WebFlux exposes `Flux<DataBuffer>` — back-pressured, event-loop-friendly. You never block a thread waiting for the full body. 2. **No ThreadLocal binding.** In MVC, `RequestContextHolder` binds the request to the current thread. In WebFlux a request may hop threads, so state travels *in the exchange* and the Reactor `Context`, not `ThreadLocal`. This is why security/context is carried reactively. 3. **Server-agnostic.** `ServerWebExchange` is not tied to `javax/jakarta.servlet`. The same handler runs on Reactor Netty (default), Undertow, or a Servlet 3.1+ container via `ServletHttpHandlerAdapter`. 4. **Immutability + mutate().** You don't reassign the URI or headers on the original request. To change them (e.g., rewrite a path in a gateway filter) you call `exchange.mutate().request(r -> r.path("/new")).build()` and pass the new exchange downstream. 5. **DataBuffer lifecycle.** `DataBuffer`s are pooled; if you read the body yourself you must release them (`DataBufferUtils.release`) to avoid memory leaks — a real gotcha. **How DispatcherHandler uses it.** `handle(ServerWebExchange exchange)` passes the same exchange to every `HandlerMapping.getHandler(exchange)`, to `HandlerAdapter.handle(exchange, handler)` (which reads the body/params off it to resolve arguments), and to `HandlerResultHandler.handleResult(exchange, result)` (which writes to `exchange.getResponse()`). The exchange is the shared mutable-attribute conduit across all three stages. **When to use directly.** Inject `ServerWebExchange` (or `ServerHttpRequest`/`ServerHttpResponse`) into a controller method when you need low-level access — raw headers, cookies, setting a response header/status manually, or writing the body yourself. Otherwise prefer higher-level bindings (`@RequestHeader`, `@CookieValue`, `ResponseEntity`). **Gotcha:** the request body is a *single-subscription* stream by default — reading it twice fails unless you cache it (e.g., `ServerWebExchangeDecorator` / `exchange.mutate()` with a cached body, or `DataBufferUtils` caching). Filters that need the body more than once must cache it explicitly.
- Why can't WebFlux rely on ThreadLocal (like RequestContextHolder) to carry request state?A reactive pipeline may switch threads between operators, so ThreadLocal state wouldn't follow the request. State travels in the ServerWebExchange and the Reactor Context instead.
- You need to read the request body twice in a filter — what's the problem and fix?The body Flux<DataBuffer> is single-subscription and consuming it once drains it. Cache it (e.g., decorate the request with a cached body via exchange.mutate() / ServerWebExchangeDecorator, releasing buffers properly) so downstream can re-read.
saying these in an interview costs you the question
- Calling getInputStream()/blocking reads on the exchange
- Assuming RequestContextHolder/ThreadLocal works in WebFlux
- Reading the body twice without caching
- Thinking ServerWebExchange wraps HttpServletRequest