skip to content

What is ServerWebExchange, and how does it differ from the servlet request/response model?

level: middleimportance: should knowfreq 50%

answer

  1. One object = request+response+attributes+session
  2. Body is Flux<DataBuffer>, not InputStream
  3. No ThreadLocal — state in exchange + Reactor Context
  4. exchange.mutate() to rewrite
  5. Release DataBuffers / body read-once

basics

~10 s

ServerWebExchange 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
java
@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

for a junior

Know it bundles the reactive request and response for one call.

for a middle

Contrast Flux<DataBuffer> vs blocking streams and list the main accessors.

for a senior

Explain no-ThreadLocal, mutate(), DataBuffer release, and single-subscription body caching.

for a principal

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

context