Spring WebFlux Integration
The fun-spring-webflux module maps dmx-fun outcomes — Option, Result, Try, and
Validated — to Spring WebFlux responses, so a handler returns a domain outcome instead
of re-implementing status/body conventions per service. Use WebfluxFun for functional
endpoints (ServerResponse) or WebfluxEntity for annotation controllers
(ResponseEntity).
It is an optional module. spring-webflux is declared compileOnly (your app
already brings Spring), and it builds on fun-reactor. The
core fun library keeps no dependency on Spring.
Adding the dependency
// Gradleimplementation("codes.domix:fun-spring-webflux:LATEST_VERSION")// Spring WebFlux — bring your own version (6.0.x – 7.0.x)implementation("org.springframework:spring-webflux:7.0.8")<!-- Maven --><dependency> <groupId>codes.domix</groupId> <artifactId>fun-spring-webflux</artifactId> <version>LATEST_VERSION</version></dependency><!-- Spring WebFlux — bring your own version (6.0.x – 7.0.x) --><dependency> <groupId>org.springframework</groupId> <artifactId>spring-webflux</artifactId> <version>7.0.8</version></dependency>
fun-spring-webfluxdeclaresspring-webfluxascompileOnlyand depends onfun-reactor. Your WebFlux application already brings Spring on the classpath, so the adapter never forces a version.
The WebfluxFun adapters
Each adapter takes the upstream reactive outcome and returns a Mono<ServerResponse>.
| Adapter | Behavior |
|---|---|
fromResult(mono, errorMapper) |
Ok → 200 + body; Err → errorMapper; empty → 404 |
fromOption(mono) |
Some → 200 + body; None or empty → 404 |
fromTry(mono, failureMapper) |
Success → 200 + body; Failure → failureMapper; empty → 404 |
fromValidated(mono) |
Valid → 200 + body; Invalid → 400 with all errors; empty → 404 |
fromResultStream(flux, errorMapper) |
aggregate a Flux<Result> (fail-fast); all Ok → 200 + list; first Err → errorMapper |
fromResultStreamAccumulating(flux) |
aggregate a Flux<Result> accumulating; all Ok → 200 + list; any Err → 400 with every error |
stream(flux, mediaType, type) |
stream a Flux<V> as the body (NDJSON / SSE); 200, each element encoded as it arrives |
The success branch defaults to 200 OK. To return a different status or headers (e.g.
201 Created + Location), fromResult, fromOption, fromTry, and fromValidated each
have an overload taking a SuccessHttpMapper<V> — see
Customizing the success response.
RouterFunction<ServerResponse> routes() { return RouterFunctions.route() .GET("/users/{id}", request -> WebfluxFun.fromResult( userService.findById(request.pathVariable("id")), // Mono<Result<User, ApiError>> error -> ServerResponse.status(error.status()).bodyValue(error.detail()))) .GET("/profiles/{id}", request -> WebfluxFun.fromOption(profileService.find(request.pathVariable("id")))) // Mono<Option<Profile>> .build();}HTTP mapping defaults
Result.Ok(v)/Try.Success(v)/Option.Some(v)/Validated.Valid(v)→200 OKwithvas the bodyOption.None→404 Not FoundValidated.Invalid(errors)→400 Bad Requestwith the accumulated errors as the bodyResult.Err(e)→ yourErrorHttpMapper<E>;Try.Failure(t)→ yourThrowableHttpMapper- an empty source
Mono(it completes without emitting an outcome) →404 Not Found
The success body is written with ServerResponse.ok().bodyValue(v), so the value must be
encodable by your WebFlux codecs.
Customizing the success response (201 Created, headers)
The default success branch is 200 OK with the value as the body. When you need a
different status or headers — 201 Created with a Location on a POST, 202 Accepted,
caching headers — pass a SuccessHttpMapper<V> to the overload of fromResult,
fromOption, fromTry, or fromValidated. It controls the Ok/Some/Success/Valid
branch; the failure branches are unchanged.
.POST("/orders", request -> WebfluxFun.fromResult( orderService.place(request), // Mono<Result<Order, ApiError>> order -> ServerResponse.created(location(order)) // Ok -> 201 Created + Location .bodyValue(order), error -> ServerResponse.status(error.status()).bodyValue(error.detail())))SuccessHttpMapper<V> mirrors ErrorHttpMapper<E> and ThrowableHttpMapper — it maps a
success value to any Mono<ServerResponse> you build.
Annotation controllers (WebfluxEntity / ResponseEntity)
WebfluxFun targets functional endpoints (ServerResponse). For @RestController
methods, WebfluxEntity is the counterpart: each adapter returns a
Mono<ResponseEntity<…>> you return straight from the handler, applying the same HTTP
conventions.
@GetMapping("/users/{id}")Mono<ResponseEntity<User>> user(@PathVariable String id) { return WebfluxEntity.fromOption(userService.find(id)); // Some -> 200, None/empty -> 404}
@GetMapping("/orders/{id}")Mono<ResponseEntity<?>> order(@PathVariable String id) { return WebfluxEntity.fromResult(orderService.findById(id), // Mono<Result<Order, ApiError>> error -> ResponseEntity.status(error.status()).body(error.detail()));}| Adapter | Behavior |
|---|---|
fromOption(mono) |
Some → 200 + body; None or empty → 404 |
fromResult(mono, onError) |
Ok → 200 + body; Err → onError; empty → 404 |
fromTry(mono, onFailure) |
Success → 200 + body; Failure → onFailure; empty → 404 |
fromValidated(mono) |
Valid → 200 + body; Invalid → 400 with all errors; empty → 404 |
fromOption preserves the body type (Mono<ResponseEntity<V>>); the others return
Mono<ResponseEntity<?>> because the success and error branches carry different body
types. The error/failure mapper builds any ResponseEntity you like (status, headers,
body).
Validating a request body → 400 with every error
Validated accumulates errors, and fromValidated turns an Invalid into a single
400 carrying all of them — no submit-three-times round-trips:
.POST("/signup", request -> request.bodyToMono(SignupForm.class) .map(form -> validate(form)) // Validated<NonEmptyList<String>, User> .flatMap(validated -> WebfluxFun.fromValidated(Mono.just(validated))))RFC 7807 problem responses
To render failures as standard RFC 7807
ProblemDetail documents (application/problem+json) instead of an ad-hoc body, use
WebfluxProblem. Its problemDetail factories return an ErrorHttpMapper /
ThrowableHttpMapper you hand to the regular adapters:
// Result.Err -> problem+json with a status and a detail derived from the error.GET("/users/{id}", request -> WebfluxFun.fromResult(userService.findById(request.pathVariable("id")), WebfluxProblem.problemDetail(HttpStatus.NOT_FOUND, ApiError::message)))
// Try.Failure -> problem+json with the exception message as the detail.POST("/orders", request -> WebfluxFun.fromTry(orderService.place(request), WebfluxProblem.problemDetail(HttpStatus.INTERNAL_SERVER_ERROR)))For accumulated validation, WebfluxProblem.fromValidated renders an Invalid as a 400
problem whose errors property lists every violation:
.POST("/signup", request -> WebfluxProblem.fromValidated( request.bodyToMono(SignupForm.class).map(form -> validate(form)))) // Mono<Validated<…>>// 400 application/problem+json — {"status":400,"detail":"Validation failed","errors":[...]}Spring Boot auto-configuration
With fun-spring-boot on the classpath, a default ThrowableHttpMapper bean
(dmxFunProblemDetailMapper) is auto-configured — a ready-made RFC 7807 renderer you can
inject and reuse instead of building one per handler:
RouterFunction<ServerResponse> routes(ThrowableHttpMapper problems) { // injected return RouterFunctions.route() .POST("/orders", request -> WebfluxFun.fromTry(orderService.place(request), problems)) .build();}The status defaults to 500 (set dmx.fun.webflux.problem.status), the bean backs off if
you declare your own ThrowableHttpMapper, and it can be disabled with
dmx.fun.webflux.problem.enabled=false. The adapters themselves stay static — this only
provides the default bean.
Aggregating a stream
fromResultStream sequences a Flux<Result<V, E>> fail-fast (via fun-reactor’s
ReactorFlux.sequence) into one response: all Ok become a 200 with the list, and the
first Err is rendered by your errorMapper.
.GET("/orders", request -> WebfluxFun.fromResultStream(orderService.findAll(query), // Flux<Result<Order, ApiError>> error -> ServerResponse.status(error.status()).bodyValue(error.detail())))To report every failure instead of stopping at the first, use the accumulating
variant — all Ok become a 200 with the list, any Err makes a 400 carrying all the
accumulated errors (backed by fun-reactor’s ReactorFlux.collectValidated):
.GET("/orders", request -> WebfluxFun.fromResultStreamAccumulating(orderService.findAll(query))) // Flux<Result<Order, ApiError>>Streaming a response body
When the response should stream element-by-element rather than collect first, stream
renders a Flux<V> as the body with a chosen content type (NDJSON or SSE):
.GET("/orders/stream", request -> WebfluxFun.stream(orderService.findAll(), MediaType.APPLICATION_NDJSON, Order.class))Because the 200 status and headers are sent before the body streams, a terminal error
cannot change the status. The four-argument overload appends a typed fallback element on
error instead of abruptly closing the connection:
WebfluxFun.stream(orders, MediaType.APPLICATION_NDJSON, Order.class, throwable -> Order.unavailable()); // graceful last element on failureComposing before responding
When the upstream is a Mono<Result<V, E>>, use fun-reactor’s ReactorResult
operators (mapOk, flatMapOk, mapErr, recover) to stay on the Result track, then
hand the result to fromResult for the HTTP mapping.
Version compatibility
Tested in CI against the following Spring Framework versions on every pull request that
touches the spring-webflux module:
| Spring version | Status |
|---|---|
| 6.0.x | tested |
| 6.1.x | tested |
| 6.2.x | tested |
| 7.0.x | tested |
To test locally against a specific version:
./gradlew :frameworks:spring-webflux:test -PspringVersion=7.0.8