How do you extract the request body inside a HandlerFunction, and when do you use bodyToMono versus bodyToFlux?
answer
- bodyToMono = single object
- bodyToFlux = array / stream (ndjson, SSE)
- lazy, non-blocking, decoded by HttpMessageReaders
- body is one-shot — consume once
- empty body -> switchIfEmpty; never .block()
basics
~10 sCall request.bodyToMono(MyType.class) when the body is a single object, or request.bodyToFlux(MyType.class) when the body is a stream/array of many objects. Both return reactive publishers you then chain onto.
solid answer
~40 sServerRequest exposes bodyToMono(Class<T>) and bodyToFlux(Class<T>) to decode the raw request body into your types using the configured HTTP message readers (e.g. Jackson for JSON). Use bodyToMono for a single object payload — it yields a Mono<T>. Use bodyToFlux for a collection/stream (a JSON array, or a streaming media type like application/x-ndjson) — it yields a Flux<T>. The decoding is lazy: nothing reads the body until you subscribe, which the framework does when it subscribes to your returned Mono<ServerResponse>. You typically flatMap the decoded body into your service call and then into the response builder. For generics use the ParameterizedTypeReference overload. Reading the body is a one-shot operation — the reactive stream can only be consumed once.
code
java · 21 linesimport org.springframework.http.HttpStatus;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
public Mono<ServerResponse> createUser(ServerRequest request) {
return request.bodyToMono(CreateUser.class) // Mono<CreateUser>
.flatMap(userService::save) // Mono<User>
.flatMap(user -> ServerResponse
.status(HttpStatus.CREATED)
.bodyValue(user))
// no body sent -> 400
.switchIfEmpty(ServerResponse.badRequest().build());
}
// Streaming many items in from an NDJSON upload:
public Mono<ServerResponse> bulkImport(ServerRequest request) {
return request.bodyToFlux(Item.class) // Flux<Item>
.flatMap(itemService::save)
.then(ServerResponse.accepted().build());
}go deeper
Know bodyToMono for one object, bodyToFlux for many, and that both return reactive types.
Explain laziness, the one-shot nature, empty-body handling with switchIfEmpty, and the ParameterizedTypeReference overload for generics.
Discuss error propagation to 400, streaming media types with bodyToFlux, and why blocking is forbidden.
Reason about backpressure on large streaming imports, choosing bodyToFlux vs Mono<List> for memory, and custom codec/HttpMessageReader configuration.
**The problem.** In the functional model you don't get `@RequestBody` binding — you pull the body off `ServerRequest` yourself, as a reactive stream. **The two extractors.** - `Mono<T> bodyToMono(Class<T> elementClass)` — decodes the body as a *single* value. Returns a `Mono<T>` (0..1 elements). Use for a normal single-object JSON payload. - `Flux<T> bodyToFlux(Class<T> elementClass)` — decodes the body as a *stream* of values. Returns a `Flux<T>` (0..N). Use for a JSON array, or a streaming content type such as `application/x-ndjson`/Server-Sent Events where elements arrive over time. - Both have `ParameterizedTypeReference<T>` overloads for generic types (e.g. `bodyToMono(new ParameterizedTypeReference<List<Item>>() {})`). Note: to read a JSON array you can *either* `bodyToFlux(Item.class)` *or* `bodyToMono` a `List` via `ParameterizedTypeReference` — the Flux form streams element-by-element; the Mono<List> buffers the whole thing. **How decoding works.** Extraction delegates to the reactive `HttpMessageReader`s registered on the `ServerCodecConfigurer` (Jackson for JSON by default). It respects the request's `Content-Type`. It is **lazy and non-blocking**: calling `bodyToMono` does not read bytes — it returns a publisher. The actual read happens when something subscribes, which the framework does when it subscribes to the `Mono<ServerResponse>` your handler returns. **Typical usage pattern.** Chain the body into your logic and fold it into the response: ```java request.bodyToMono(CreateOrder.class) .flatMap(orderService::create) // Mono<Order> .flatMap(order -> ServerResponse.ok().bodyValue(order)); ``` **Edge cases & gotchas.** - *One-shot*: the request body is a single-consumption stream. Calling `bodyToMono` twice, or `bodyToMono` then `bodyToFlux`, does not re-read the network body — don't try to consume it multiple times. - *Empty body*: for a `GET`/`DELETE` with no body, `bodyToMono` completes empty (emits nothing). Downstream operators that assume a value (`flatMap` producing the response) will be skipped — you often need `switchIfEmpty(...)` to return a 400/response when a body was required. - *Errors*: malformed JSON causes the returned publisher to emit an error (e.g. `ServerWebInputException` / `DecodingException`), which propagates and — unless handled — yields a 400. Handle with `onErrorResume` if you need a custom response. - *Blocking is forbidden*: never call `.block()` on the body inside a handler — that defeats the non-blocking model and can deadlock on event-loop threads. - *Streaming semantics*: `bodyToFlux` truly streams for streaming media types, applying backpressure; for a plain JSON array Jackson still parses incrementally.
- What happens if the JSON body is malformed?The publisher returned by bodyToMono/bodyToFlux emits an error (a DecodingException wrapped as ServerWebInputException). If unhandled it maps to HTTP 400 Bad Request. You can intercept it with onErrorResume to shape a custom error response.
- Why can't you just call request.bodyToMono(...).block() to get the value synchronously?Blocking on an event-loop thread stalls the non-blocking runtime and risks deadlock/thread starvation. WebFlux expects you to compose with flatMap/map and return a Mono; the framework subscribes for you.
saying these in an interview costs you the question
- Calling .block() on the body inside a handler
- Trying to read the body twice (it's a one-shot stream)
- Using bodyToMono for a streaming/array payload where per-element processing is needed
- Forgetting the empty-body case (no switchIfEmpty), so a required body silently produces no response