namalzi

← All posts

Digging into Spring's Source 24: WebFlux — DispatcherHandler, Functional Endpoints, and WebClient

Spring · Published

Follow the path a request takes through Spring WebFlux, in the source. We look at how WebFilter and WebExceptionHandler are woven together under HttpHandler, at DispatcherHandler as the reactive counterpart of DispatcherServlet, at how annotated controllers and functional endpoints share one road, and at the filter chain inside WebClient.

What this post covers

This is the twenty-fourth post in the "Digging into Spring's Source" series. Part 21, Part 22, and Part 23 showed how Spring MVC, which runs on top of servlets, handles a single request. This post reads Spring WebFlux, which does the same job in a reactive way. On the map in Part 1, this is U22 in Phase 5 (web).

There are four questions to answer.

Item Details
Reference source Spring Framework 7.1.0-SNAPSHOT
Commit f447f3c310 (2026-10-01, main branch)
Modules read most spring-webflux, spring-web (http.server.reactive, web.server, http.codec)
Classes read most WebHttpHandlerBuilder, HttpWebHandlerAdapter, DispatcherHandler, RequestMappingHandlerAdapter, RouterFunctions, RouterFunctionMapping, DefaultWebClient

Every line number and behavior in this post refers to that commit. In other versions the line numbers will differ and the details may too. Part 1 explains how to check out the same commit.

WebFlux is wide. This post keeps to the main line that someone new to Spring can follow, and what was left out is listed by name in "What this post leaves out" at the end.

Words to know first

Reactive, Mono, and Flux. An ordinary method waits until the result is ready and then returns the value. A reactive method returns right away, and what it returns is not the value but "a channel the value will flow through later". In the Reactor library those channels are Mono (zero or one value) and Flux (many values). We first met them in Part 19, section 5, with reactive transactions.

Nothing runs until someone subscribes. Holding a Mono means neither that the work is finished nor that it has started. A Mono is a plan — "this is what I will do" — and it only starts running when someone subscribes to it. The examples in this post let you see that twice. They use block(), which means "subscribe, and wait on this thread until it is done".

Three signals. What flows through the channel is a value (onNext), normal completion (onComplete), or an error (onError). In reactive code an exception does not climb the call stack with throw; it travels along the channel as an error signal. So instead of try/catch you receive it with an operator such as onErrorResume. A Mono<Void> is a channel that carries no value, only "done" or "failed".

Operators. These are the methods you chain onto a channel to extend the plan. Only the ones that appear in this post's source are listed.

Operator Meaning
map / flatMap Transform the value when it arrives / use the value to create another channel and continue with it
concatMap Create a channel per Flux value, but start the next only after the previous one ends
next Take only the first value of a Flux as a Mono and cancel the rest
switchIfEmpty If it ends without a value, switch to another channel
onErrorResume If an error signal arrives, switch to another channel (the equivalent of catch)
then When the previous one finishes, run the next
Mono.defer Run the code inside, and create the channel, only on subscription

ServerWebExchange bundles one request, its response, and a map of attributes used during that request (ServerWebExchange line 44, lines 65–75). Where MVC carried HttpServletRequest and HttpServletResponse around as a pair, WebFlux carries this one object.

Handler, HandlerMapping, HandlerAdapter mean the same as in MVC. The handler is our code that actually processes the request, a HandlerMapping finds "who handles this request", and a HandlerAdapter knows how to call that handler. Parts 21 and 22 covered them. WebFlux has its own interfaces with the same names, in the org.springframework.web.reactive package.

1. The floor: everything comes down to one HttpHandler

At the very bottom of WebFlux is an interface with a single method.

public interface HttpHandler {

	/**
	 * Handle the given request and write to the response.
	 * @param request current request
	 * @param response current response
	 * @return indicates completion of request handling
	 */
	Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response);

}

spring-web/src/main/java/org/springframework/http/server/reactive/HttpHandler.java lines 43–53

It takes a request and a response and returns a Mono<Void> that says "I will tell you when it is all done". It resembles a servlet's service(request, response), but the fact that the return type is not void changes everything. When the method returns, nothing has finished yet.

Notice that this interface lives in spring-web, not in spring-webflux. So do WebFilter, WebHandler, and ServerWebExchange. That is why spring-web was the shared floor under both MVC and WebFlux on the module map in Part 2. spring-webflux holds the dispatcher and the programming models that sit on top.

The layers from the server down to the handler. The server at the top (Reactor Netty, Tomcat, Jetty) calls a server adapter, which wraps the server's request and response in Spring types and passes them to an HttpHandler. HttpWebHandlerAdapter, the HttpHandler implementation, creates a ServerWebExchange and passes it to ExceptionHandlingWebHandler, which passes it to FilteringWebHandler, which runs the WebFilters and finally reaches the last WebHandler, DispatcherHandler. An error that nobody handled is ended as a 500 by HttpWebHandlerAdapter. The two blue boxes are where we can step in (exception handlers and filters). The red box is the last safety net.

Server adapters — one per server

HttpHandler knows nothing about any particular server. The connection is made by an adapter, one per server. The http.server.reactive package of spring-web has ReactorHttpHandlerAdapter (Reactor Netty), TomcatHttpHandlerAdapter, JettyCoreHttpHandlerAdapter, and ServletHttpHandlerAdapter. They all do the same thing: wrap the server's own request and response objects in Spring's ServerHttpRequest and ServerHttpResponse, then call httpHandler.handle(request, response) (ReactorHttpHandlerAdapter lines 55–76).

So who subscribes? The servlet adapter does it itself — this.httpHandler.handle(httpRequest, httpResponse).subscribe(subscriber) (ServletHttpHandlerAdapter line 197). The Reactor Netty adapter returns the Mono as it is (lines 65–67), and subscribing after that is up to Reactor Netty, which is outside this repository.

WebHttpHandlerBuilder — three layers of wrapping

We rarely implement HttpHandler ourselves. We work with three interfaces one level up, and WebHttpHandlerBuilder weaves them into a single HttpHandler.

Interface Method Role
WebHandler Mono<Void> handle(ServerWebExchange) The body that handles the request. Usually DispatcherHandler (lines 35–44)
WebFilter Mono<Void> filter(ServerWebExchange, WebFilterChain) A filter that steps in before and after the body (lines 32–43)
WebExceptionHandler Mono<Void> handle(ServerWebExchange, Throwable) Turns an error signal into a response (lines 27–39)

The weaving happens in build(). (A // ... line marks something this post trimmed; the rest is the source as it is.)

	public HttpHandler build() {
		WebHandler decorated = new FilteringWebHandler(this.webHandler, this.filters);
		decorated = new ExceptionHandlingWebHandler(decorated, this.exceptionHandlers);

		HttpWebHandlerAdapter adapted = new HttpWebHandlerAdapter(decorated);
		// ...
		adapted.afterPropertiesSet();

		return (this.httpHandlerDecorator != null ? this.httpHandlerDecorator.apply(adapted) : adapted);
	}

spring-web/src/main/java/org/springframework/web/server/adapter/WebHttpHandlerBuilder.java lines 427–459

There are three layers, from the inside out.

  1. FilteringWebHandler wraps the body in a filter chain. The chain is a DefaultWebFilterChain. It walks the filter list from the back, building "this filter + the link after it" one at a time and putting it in front (lines 71–78), so the filters run in the order you registered them. One link's filter() calls the remaining filter if there is one, and the body if there is not (lines 103–108).
  2. ExceptionHandlingWebHandler wraps that and receives error signals. We read it just below.
  3. HttpWebHandlerAdapter is the adapter that turns a WebHandler into an HttpHandler. For each request it creates a new ServerWebExchange (line 306), hands it inward, and completes the response at the end (lines 321–326).

The body of ExceptionHandlingWebHandler is short enough to read whole.

	@Override
	public Mono<Void> handle(ServerWebExchange exchange) {
		Mono<Void> completion;
		try {
			completion = super.handle(exchange);
		}
		catch (Throwable ex) {
			completion = Mono.error(ex);
		}

		for (WebExceptionHandler handler : this.exceptionHandlers) {
			completion = completion.doOnError(error -> exchange.getAttributes().put(HANDLED_WEB_EXCEPTION, error))
					.onErrorResume(ex -> handler.handle(exchange, ex));
		}
		return completion;
	}

spring-web/src/main/java/org/springframework/web/server/handler/ExceptionHandlingWebHandler.java lines 73–88

Two things to notice. First, there is a try/catch. It covers the case where inner code simply throws before it even returns a Mono, and turns that into an error signal (Mono.error) so there is one road for both. Second, the for loop does not run the exception handlers. It only chains one onErrorResume per handler. If an error occurs the first handler receives it, and if that handler returns an error signal again (meaning "not mine"), the next onErrorResume receives it. The comment on WebExceptionHandler says the same: a completion signal means "handled", an error signal means "still not handled" (lines 30–32).

If nobody handles it all the way out, handleUnresolvedError in the outermost HttpWebHandlerAdapter receives it, sets the status code to 500, and ends there (line 323, lines 359–382).

Example 1: the order of filters and exception handlers

Without starting a server, we build an HttpHandler with WebHttpHandlerBuilder and call it directly with the fake request and response from spring-test (MockServerHttpRequest, MockServerHttpResponse). This is ex/u22/FilterChainExample.java. The example code in this post is trimmed to the essentials (imports and the class and main declarations are left out and the indentation is pulled in; other trimmed spots are marked with // ... comments, and short methods are joined onto one line).

// An example written for this post (not Spring's source)
/** A filter that prints its name before and after. */
static WebFilter named(String name) {
    return (exchange, chain) -> {
        System.out.println("  " + name + " before");
        return chain.filter(exchange)
                .doOnSuccess(v -> System.out.println("  " + name + " after (completed)"))
                .doOnError(ex -> System.out.println("  " + name + " after (error " + ex.getClass().getSimpleName() + ")"));
    };
}

static void send(HttpHandler httpHandler) {
    MockServerHttpRequest request = MockServerHttpRequest.get("/hello").build();
    MockServerHttpResponse response = new MockServerHttpResponse();
    Mono<Void> completion = httpHandler.handle(request, response);
    System.out.println("  handle() returned a Mono. status so far = " + response.getStatusCode());
    completion.block();          // the subscription happens here
    System.out.println("  status = " + response.getStatusCode()
            + ", body = \"" + response.getBodyAsString().block() + "\"");
}

// ... from here on, inside main. hello is a WebHandler that writes 200 and "hello",
//     notFound is a WebHandler that returns Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND))

System.out.println("[1] two filters, then the handler");
send(WebHttpHandlerBuilder.webHandler(hello)
        .filter(named("filter A"), named("filter B"))
        .build());

System.out.println("[2] the handler fails, a WebExceptionHandler is registered");
send(WebHttpHandlerBuilder.webHandler(notFound)
        .filter(named("filter A"))
        .exceptionHandler(new ResponseStatusExceptionHandler())
        .build());

System.out.println("[3] the handler fails, no WebExceptionHandler");
send(WebHttpHandlerBuilder.webHandler(notFound)
        .filter(named("filter A"))
        .build());

This example was run with the released Spring Framework 7.0.9 to confirm the output below (the 7.1.0-SNAPSHOT this series reads was not built and run directly). The same goes for the other examples in this post.

[1] two filters, then the handler
  handle() returned a Mono. status so far = null
  filter A before
  filter B before
  handler
  filter B after (completed)
  filter A after (completed)
  status = 200 OK, body = "hello"
[2] the handler fails, a WebExceptionHandler is registered
  handle() returned a Mono. status so far = null
  filter A before
  handler (signals ResponseStatusException 404)
  filter A after (error ResponseStatusException)
  status = 404 NOT_FOUND, body = ""
[3] the handler fails, no WebExceptionHandler
  handle() returned a Mono. status so far = null
  filter A before
  handler (signals ResponseStatusException 404)
  filter A after (error ResponseStatusException)
  status = 500 INTERNAL_SERVER_ERROR, body = ""

Easy to get wrong

2. DispatcherHandler.handle — the reactive doDispatch

What usually sits in the innermost WebHandler slot from section 1 is DispatcherHandler. Its class declaration says it implements WebHandler (line 72). Unlike MVC's DispatcherServlet, a servlet with four levels of inheritance, it is a single 220-line class, and the handle() that processes a request is fourteen lines long.

	@Override
	public Mono<Void> handle(ServerWebExchange exchange) {
		if (this.handlerMappings == null) {
			return createNotFoundError();
		}
		if (CorsUtils.isPreFlightRequest(exchange.getRequest())) {
			return handlePreFlight(exchange);
		}
		return Flux.fromIterable(this.handlerMappings)
				.concatMap(mapping -> mapping.getHandler(exchange))
				.next()
				.switchIfEmpty(createNotFoundError())
				.onErrorResume(ex -> handleResultMono(exchange, Mono.error(ex)))
				.flatMap(handler -> handleRequestWith(exchange, handler));
	}

spring-webflux/src/main/java/org/springframework/web/reactive/DispatcherHandler.java lines 137–151

The last six lines are all of it. Line by line:

  1. Flux.fromIterable(this.handlerMappings) — puts the list of HandlerMappings on a channel.
  2. .concatMap(mapping -> mapping.getHandler(exchange)) — asks them one at a time, "do you know the handler for this request?" getHandler returns a Mono<Object>, and it ends without a value if the mapping does not know (HandlerMapping line 107).
  3. .next() — takes only the first handler that comes out and does not ask the remaining mappings.
  4. .switchIfEmpty(createNotFoundError()) — if nobody knows, switches to an error signal carrying ResponseStatusException(HttpStatus.NOT_FOUND) (lines 153–158).
  5. .onErrorResume(...) — gives the exception-handling side one chance at any error up to this point (the 404 included).
  6. .flatMap(handler -> handleRequestWith(exchange, handler)) — if a handler was found, goes on to run it.

"Ask each in turn and use the first one that answers", which MVC's getHandler() did with a for loop, has become concatMap and next. The loop and the return turned into operators; the meaning is the same.

The flow of DispatcherHandler.handle. It receives a ServerWebExchange and asks each HandlerMapping in turn. If nobody knows, the result is a 404 ResponseStatusException error signal. If one handler comes out, handleRequestWith picks the first HandlerAdapter whose supports is true and runs the handler with adapter.handle. The HandlerResult that comes back is passed by handleResult to the first HandlerResultHandler whose supports is true, which writes to the response and returns a Mono carrying only a done signal. An error signal while running the handler goes to a DispatchExceptionHandler, that is the @ExceptionHandler methods; if a HandlerResult comes out of that it goes to handleResult again, and if no method matches it leaves for the WebExceptionHandlers outside. Solid lines are the normal flow; dotted lines are the road an error signal takes.

Calling the handler, then handling the result

	private Mono<Void> handleRequestWith(ServerWebExchange exchange, Object handler) {
		if (ObjectUtils.nullSafeEquals(exchange.getResponse().getStatusCode(), HttpStatus.FORBIDDEN)) {
			return Mono.empty();  // CORS rejection
		}
		if (this.handlerAdapters != null) {
			for (HandlerAdapter adapter : this.handlerAdapters) {
				if (adapter.supports(handler)) {
					Mono<HandlerResult> resultMono = adapter.handle(exchange, handler);
					return handleResultMono(exchange, resultMono);
				}
			}
		}
		return Mono.error(new IllegalStateException("No HandlerAdapter: " + handler));
	}

spring-webflux/src/main/java/org/springframework/web/reactive/DispatcherHandler.java lines 196–209

The first HandlerAdapter whose supports(handler) is true runs the handler. So far this is the same as MVC. What differs is what comes back. MVC's adapter returned a ModelAndView; WebFlux's adapter returns a Mono<HandlerResult> (HandlerAdapter line 71). A HandlerResult is a container for "which handler returned what, with which declared type" (HandlerResult lines 32–40).

Turning that container into a response is the job of a third strategy, HandlerResultHandler. handleResult() walks the list and hands the result to the first one whose supports(result) is true (lines 181–194). MVC has no interface with this name. In MVC, return value handling lived inside the adapter (HandlerMethodReturnValueHandler); WebFlux pulled it out into a strategy the dispatcher picks directly. So the WebFlux dispatcher has three kinds of strategies.

Errors while running the handler are dealt with by handleResultMono() (lines 160–179). If an adapter also implements DispatchExceptionHandler, it gets the first chance (this is how @ExceptionHandler methods are called; see section 3). An error signal that is still left flows out of DispatcherHandler, and the WebExceptionHandlers from section 1 receive it.

Side by side with MVC

Spring MVC (Part 21) Spring WebFlux (this post)
Dispatcher DispatcherServlet (a servlet) DispatcherHandler (a WebHandler)
Central method void doDispatch(request, response) Mono<Void> handle(exchange)
Request and response HttpServletRequest, HttpServletResponse one ServerWebExchange
Finding the handler HandlerMapping → HandlerExecutionChain (handler + interceptors) HandlerMapping → Mono<Object> (handler only)
Calling the handler HandlerAdapter → ModelAndView HandlerAdapter → Mono<HandlerResult>
Handling the result return value handlers inside the adapter, then view rendering HandlerResultHandler
Stepping in before and after HandlerInterceptor, servlet Filter WebFilter
Exceptions HandlerExceptionResolver DispatchExceptionHandler, then WebExceptionHandler outside it
Body conversion HttpMessageConverter HttpMessageReader / HttpMessageWriter
What "done" means the method returned the returned Mono emitted its completion signal

"Handler only" stands out in that table. A WebFlux HandlerMapping does not return a bundle of interceptors, and the spring-webflux module has no type called HandlerInterceptor at all. Stepping in around the handler is the job of the WebFilter from section 1.

Easy to get wrong

3. Where the strategy beans come from, and annotated controllers

What @EnableWebFlux registers

DispatcherHandler does not create its three lists itself. When it is given an ApplicationContext, initStrategies() finds every bean of type HandlerMapping, HandlerAdapter, and HandlerResultHandler and keeps them sorted by order (lines 115–134). MVC had a defaults file (DispatcherServlet.properties) for when no beans were present, but DispatcherHandler has no such fallback. No beans, empty lists.

@EnableWebFlux is what registers those beans. The annotation @Imports DelegatingWebFluxConfiguration (EnableWebFlux line 90), and the @Bean methods are gathered in its parent, WebFluxConfigurationSupport. Part 9 showed how @Import pulls in a configuration class. Picking only the ones that matter for this post:

Bean Kind Location
webHandler → DispatcherHandler the dispatcher itself lines 125–128
WebFluxResponseStatusExceptionHandler WebExceptionHandler lines 130–134
RouterFunctionMapping (order -1) HandlerMapping lines 248–258
RequestMappingHandlerMapping (order 0) HandlerMapping lines 136–142
RequestMappingHandlerAdapter, HandlerFunctionAdapter, SimpleHandlerAdapter HandlerAdapter line 305, lines 482–490
ResponseEntityResultHandler, ResponseBodyResultHandler, ViewResolutionResultHandler, ServerResponseResultHandler HandlerResultHandler lines 521–562

It is no accident that the dispatcher bean is named webHandler. WebHttpHandlerBuilder.applicationContext(context) looks up the body by exactly that name (WEB_HANDLER_BEAN_NAME, line 72) and collects the WebFilter and WebExceptionHandler beans by type, sorted (lines 174–189). What we put in by hand in section 1 is taken from the container here.

The road an annotated controller takes

The road to a method with @GetMapping has the same names as in MVC. Only the implementations are the reactive ones.

  1. RequestMappingHandlerMapping finds the HandlerMethod that matches the request. Even the inheritance has the same shape as MVC (RequestMappingHandlerMapping → RequestMappingInfoHandlerMapping → AbstractHandlerMethodMapping → AbstractHandlerMapping, line 75, line 64, line 67). The matching rules follow the same idea as in Part 22.
  2. RequestMappingHandlerAdapter takes its turn when handler instanceof HandlerMethod (lines 247–250). These five lines are the core.
		Mono<HandlerResult> resultMono = this.modelInitializer
				.initModel(handlerMethod, bindingContext, exchange)
				.then(Mono.defer(() -> invocableMethod.invoke(exchange, bindingContext)))
				.doOnNext(result -> result.setExceptionHandler(exceptionHandler))
				.onErrorResume(ex -> exceptionHandler.handleError(exchange, ex));

spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java lines 270–274

Prepare the model, then call the method, attach an exception handler to the result, and on an error hand over to that handler. The exceptionHandler here is a lambda that calls handleException() in the same class (lines 267–268), which finds the matching @ExceptionHandler method and runs it (lines 294–295). The method itself is called by InvocableHandlerMethod.invoke(), which, once all arguments are ready, does method.invoke(getBean(), args) by reflection (InvocableHandlerMethod lines 181–205). Whatever the return value is, it comes out inside a HandlerResult (lines 239–240).

  1. ResponseBodyResultHandler takes its turn when the class or the method has @ResponseBody (@RestController carries it) (lines 98–104), and passes the return value to writeBody() (line 118).

HttpMessageWriter — where the return value becomes bytes

writeBody() is in the parent class AbstractMessageWriterResultHandler (from line 169). It does three things.

HttpMessageWriter and HttpMessageReader correspond to MVC's HttpMessageConverter (Part 20). In MVC, reading and writing were one interface; here they are split in two, and above all what is passed around is a channel. The writing side takes a Publisher, not a single object (HttpMessageWriter line 89), and the reading side returns a Flux<T> or Mono<T> (HttpMessageReader line 86, line 96). The point is not to hold a thread waiting until the whole body has arrived.

The path to an annotated controller in WebFlux. @EnableWebFlux registers the @Bean methods of WebFluxConfigurationSupport, and DispatcherHandler.initStrategies() collects them by type and sorts them. ① RequestMappingHandlerMapping finds the HandlerMethod for the request. ② RequestMappingHandlerAdapter (lines 270–274) runs modelInitializer.initModel, then calls the method with invocableMethod.invoke(exchange, bindingContext), and attaches an exception handler to the result with setExceptionHandler. On an error, onErrorResume hands over to exceptionHandler.handleError, which finds an @ExceptionHandler method. The outcome is a HandlerResult. ③ ResponseBodyResultHandler's writeBody (in AbstractMessageWriterResultHandler) uses a reactive type as the Publisher as is and wraps a plain object in Mono.justOrEmpty(body), picks the response media type, and lets the first HttpMessageWriter in the list whose canWrite(type, media type) is true write it The names are the same as in MVC; the only difference is that each step is chained with Mono. This puts steps 1–3 of this section (①②③ in the figure) on one page.

4. Functional endpoints — functions instead of annotations

WebFlux has one more way to write a controller. Instead of annotations, you assemble it from three functions.

Interface Method In one line
HandlerFunction<T> Mono<T> handle(ServerRequest) A function that takes a request and produces a response (lines 30–39)
RequestPredicate boolean test(ServerRequest) A function that answers "does this request match?" (lines 33–40)
RouterFunction<T> Mono<HandlerFunction<T>> route(ServerRequest) A function that takes a request and returns the HandlerFunction that will handle it (lines 36–44)

All three are @FunctionalInterfaces, so you can write them as lambdas. ServerRequest and ServerResponse are the request and response types that wrap ServerWebExchange to suit the functional model.

The smallest RouterFunction is "one condition + one handler".

		@Override
		public Mono<HandlerFunction<T>> route(ServerRequest request) {
			if (this.predicate.test(request)) {
				// ...
				return Mono.just(this.handlerFunction);
			}
			else {
				return Mono.empty();
			}
		}

spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java lines 1295–1307

If the condition matches it returns the handler, otherwise an empty Mono. RouterFunctions.route(predicate, handlerFunction) creates this DefaultRouterFunction (lines 116–120).

With several routes you use the builder. RouterFunctions.route() (the no-argument one) returns a RouterFunctionBuilder (lines 97–99), and the builder's GET("/path", handler) creates one of those small RouterFunctions with a RequestPredicates.GET(pattern) condition and adds it to a list (RouterFunctionBuilder lines 84–86, lines 57–67). build() ties that list into one BuiltRouterFunction (lines 414–428).

		@Override
		public Mono<HandlerFunction<ServerResponse>> route(ServerRequest request) {
			return Flux.fromIterable(this.routerFunctions)
					.concatMap(routerFunction -> routerFunction.route(request))
					.next();
		}

spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctionBuilder.java lines 444–449

It has the same shape as DispatcherHandler.handle() in section 2: ask the list in turn and use the first that answers. So with functional endpoints, registration order is the priority. That differs from the annotation side, which picks the most specific of several candidates for you.

How a functional endpoint is built and used. RouterFunctions.route() creates a RouterFunctionBuilder, and each call to GET(pattern, handler) produces one DefaultRouterFunction holding a RequestPredicate and a HandlerFunction. build() ties them into a BuiltRouterFunction, a list in registration order. route(request) walks the list from the front calling predicate.test(request); on true it stops with Mono.just(handlerFunction), and on false the result is Mono.empty() so it moves on to the next route. The chosen handlerFunction.handle(request) returns a Mono. It stops at the green box. Later routes are not even asked.

The three pieces that plug into DispatcherHandler

Functional endpoints do not use a separate dispatcher. They are simply plugged into the three strategy slots from section 2, one piece each.

  1. RouterFunctionMapping (a HandlerMapping) collects every RouterFunction bean in the container and merges them into one with andOther (lines 122–134). When a request arrives it creates a ServerRequest and calls routerFunction.route(request). The HandlerFunction that comes out is the handler, and the ServerRequest it just created is stored in the exchange's attributes (lines 158–172).
  2. HandlerFunctionAdapter (a HandlerAdapter) is twelve lines even when quoted in full.
	@Override
	public boolean supports(Object handler) {
		return handler instanceof HandlerFunction;
	}

	@Override
	public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) {
		HandlerFunction<?> handlerFunction = (HandlerFunction<?>) handler;
		ServerRequest request = exchange.getRequiredAttribute(RouterFunctions.REQUEST_ATTRIBUTE);
		return handlerFunction.handle(request)
				.map(response -> new HandlerResult(handlerFunction, response, HANDLER_FUNCTION_RETURN_TYPE));
	}

spring-webflux/src/main/java/org/springframework/web/reactive/function/server/support/HandlerFunctionAdapter.java lines 52–63

It takes the ServerRequest the mapping stored out of the attributes, calls the function, and puts the returned ServerResponse into a HandlerResult. Compare the annotation-side adapter, which was long because it resolves arguments, prepares the model, and validates. In the functional model our own function does all that directly with the ServerRequest, so the adapter has nothing left to do. 3. ServerResponseResultHandler (a HandlerResultHandler) takes its turn when the return value is a ServerResponse (lines 104–107), and calls response.writeTo(exchange, context) so that the response writes itself (lines 109–123). writeTo first copies the status code and headers (DefaultServerResponseBuilder lines 327–344) and then writes the body with the list of HttpMessageWriters it was handed. Those are the same writers as in section 3.

Two programming models under one DispatcherHandler. On the left, RouterFunctionMapping with order -1 finds a HandlerFunction, HandlerFunctionAdapter calls it and gets a ServerResponse, and ServerResponseResultHandler calls response.writeTo. On the right, RequestMappingHandlerMapping with order 0 finds a HandlerMethod, RequestMappingHandlerAdapter calls it and gets a @ResponseBody return value, and ResponseBodyResultHandler calls writeBody. On both sides an HttpMessageWriter finally turns the object into response bytes. Blue is the functional side, gray the annotation side. They meet again at the bottom.

Example 2: both models pass through the same dispatcher

This is ex/u22/DispatcherHandlerExample.java. One @EnableWebFlux configuration registers a controller and a RouterFunction together, and on purpose both of them declare the same path, /both.

// An example written for this post (not Spring's source)
@RestController
static class HelloController {
    @GetMapping("/hello")
    Mono<String> hello() { return Mono.just("hello from @GetMapping"); }

    @GetMapping("/both")
    Mono<String> both() { return Mono.just("both from @GetMapping"); }
}

@Configuration
@EnableWebFlux
static class Config {
    @Bean
    HelloController helloController() { return new HelloController(); }

    @Bean
    RouterFunction<ServerResponse> routes() {
        return RouterFunctions.route()
                .GET("/fn/hello", request -> ServerResponse.ok().bodyValue("hello from HandlerFunction"))
                .GET("/both", request -> ServerResponse.ok().bodyValue("both from HandlerFunction"))
                .build();
    }

    /** A filter that, after the chain ends, prints the kind of handler the HandlerMapping chose. */
    @Bean
    WebFilter handlerTracer() {
        return (exchange, chain) -> chain.filter(exchange).doFinally(signal -> {
            Object handler = exchange.getAttribute(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE);
            // ... print whether it is a HandlerMethod or a HandlerFunction
        });
    }
}

// ... main: collect and print the three kinds of strategy beans the way DispatcherHandler.initStrategies does,
//     then call the lines below. send, as in example 1, sends a fake request and prints the status and body
HttpHandler httpHandler = WebHttpHandlerBuilder.applicationContext(context).build();
send(httpHandler, "/hello");
send(httpHandler, "/fn/hello");
send(httpHandler, "/both");
send(httpHandler, "/nope");
[1] bean "webHandler" is a DispatcherHandler
  same list as getHandlerMappings()? true
  HandlerMapping:
    RouterFunctionMapping
    RequestMappingHandlerMapping
    EmptyHandlerMapping
  HandlerAdapter:
    WebSocketHandlerAdapter
    RequestMappingHandlerAdapter
    HandlerFunctionAdapter
    SimpleHandlerAdapter
  HandlerResultHandler:
    ResponseEntityResultHandler
    ServerResponseResultHandler
    ResponseBodyResultHandler
    ViewResolutionResultHandler
[2] requests through WebHttpHandlerBuilder.applicationContext(context)
GET /hello
  handler kind = HandlerMethod
  status = null, body = "hello from @GetMapping"
GET /fn/hello
  handler kind = HandlerFunction
  status = 200 OK, body = "hello from HandlerFunction"
GET /both
  handler kind = HandlerFunction
  status = 200 OK, body = "both from HandlerFunction"
GET /nope
  handler kind = null
  status = 404 NOT_FOUND, body = ""

(When you run it, two log lines from Hibernate Validator announcing its version are printed first. They have nothing to do with this post and are left out.)

Easy to get wrong

5. WebClient — the side that sends requests

WebClient is the reactive HTTP client that ships in spring-webflux. Compared with the server side only the direction is reversed; the ideas are the same. There is a filter chain, at the very end there is an adapter that connects to a real HTTP library, and the body is read and written by the same HttpMessageReaders and HttpMessageWriters.

Server side (section 1) Client side Method
WebHandler ExchangeFunction Mono<ClientResponse> exchange(ClientRequest) (lines 42–55)
WebFilter ExchangeFilterFunction Mono<ClientResponse> filter(ClientRequest, ExchangeFunction next) (lines 35–51)
Server adapter ClientHttpConnector Mono<ClientHttpResponse> connect(method, uri, requestCallback) (lines 34–49)

What the builder assembles

WebClient.create() and WebClient.builder() both start from DefaultWebClientBuilder (WebClient lines 147–149, lines 164–166). The first part of build() prepares the three things in the table above.

	public WebClient build() {
		ClientHttpConnector connectorToUse =
				(this.connector != null ? this.connector : initConnector());

		ExchangeFunction exchange = (this.exchangeFunction == null ?
				ExchangeFunctions.create(connectorToUse, initExchangeStrategies()) :
				this.exchangeFunction);

		ExchangeFilterFunction filterFunctions = (this.filters != null ? this.filters.stream()
				.reduce(ExchangeFilterFunction::andThen)
				.orElse(null) : null);
		// ...
	}

spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java lines 304–327

One request

The object you write the request on with client.get().uri(...) is DefaultRequestBodyUriSpec, inside DefaultWebClient (line 207). The actual sending is done by its exchange() (lines 442–480). Apart from its first line, which prepares the request builder (line 443), the whole body of that method sits inside Mono.deferContextual(...), so before subscription not even the ClientRequest is built (requestBuilder.build() is on line 456). On subscription it puts the filters over the ExchangeFunction and calls it — finalFilterFunction.apply(exchangeFunction).exchange(request) (lines 462–463).

The flow of one WebClient request. client.get().uri(...) writes the request down, and exchange() builds the ClientRequest on subscription. The request passes ExchangeFilterFunction A and then B and reaches the ExchangeFunction, DefaultExchangeFunction, which calls ClientHttpConnector.connect and goes out to the HTTP server. Who reads the ClientResponse that comes back splits two ways. With retrieve(), a ResponseSpec reads it, and a status of 400 or above becomes a WebClientResponseException. With exchangeToMono(function), your own function sees the status and body itself. The blue boxes are the filters. The response comes back along the same road in reverse.

retrieve() and exchangeToMono()

Both call the same exchange(). What differs is who deals with the ClientResponse that comes back.

		private static final StatusHandler DEFAULT_STATUS_HANDLER =
				new StatusHandler(code -> code.value() >= 400, ClientResponse::createException);

spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java lines 546–547

When you take the body out with bodyToMono(...), the status handlers are applied first (lines 594–598, lines 677–681), so with a status of 400 or above you get a WebClientResponseException error signal instead of the body. A handler you add with onStatus(...) is placed ahead of the default one (lines 572–580).

Example 3: start a server briefly and call it

This is ex/u22/WebClientExample.java. It starts a Reactor Netty server on an arbitrary free port (port(0)), hooks a functional endpoint to it with the ReactorHttpHandlerAdapter from section 1, and calls it with WebClient. The server is closed at the end.

// An example written for this post (not Spring's source)
/** A filter that prints its name when the request goes out and when the response comes back. */
static ExchangeFilterFunction named(String name) {
    return (request, next) -> {
        System.out.println("  " + name + " -> " + request.method() + " " + request.url().getPath());
        return next.exchange(request)
                .doOnNext(response -> System.out.println("  " + name + " <- " + response.statusCode().value()));
    };
}

// ... from here on, inside main
AtomicInteger hits = new AtomicInteger();              // how many requests the server received
RouterFunction<ServerResponse> routes = RouterFunctions.route()
        .GET("/greet/{name}", request -> {
            hits.incrementAndGet();
            return ServerResponse.ok().bodyValue("hi, " + request.pathVariable("name"));
        })
        .build();

HttpHandler httpHandler = RouterFunctions.toHttpHandler(routes);
DisposableServer server = HttpServer.create()
        .host("localhost")
        .port(0)                                              // any free port
        .handle(new ReactorHttpHandlerAdapter(httpHandler))
        .bindNow(TIMEOUT);

// ... what follows is inside a try block; the last three lines, which close the server, are in its finally block
WebClient client = WebClient.builder()
        .baseUrl("http://localhost:" + server.port())
        .filter(named("filter A"))
        .filter(named("filter B"))
        .build();

System.out.println("[1] retrieve(): nothing is sent until someone subscribes");
Mono<String> greeting = client.get().uri("/greet/{name}", "spring")
        .retrieve()
        .bodyToMono(String.class);
System.out.println("  Mono built. server hits = " + hits.get());
System.out.println("  body = \"" + greeting.block(TIMEOUT) + "\", server hits = " + hits.get());

System.out.println("[2] subscribing to the same Mono again sends a second request");
System.out.println("  body = \"" + greeting.block(TIMEOUT) + "\", server hits = " + hits.get());

System.out.println("[3] retrieve() on a 404");
try {
    client.get().uri("/nope").retrieve().bodyToMono(String.class).block(TIMEOUT);
}
catch (WebClientResponseException ex) {
    System.out.println("  " + ex.getClass().getSimpleName() + ", status = " + ex.getStatusCode().value());
}

System.out.println("[4] exchangeToMono() on a 404");
String seen = client.get().uri("/nope")
        .exchangeToMono(response -> Mono.just("my function saw status " + response.statusCode().value()))
        .block(TIMEOUT);
System.out.println("  " + seen);

server.disposeNow(TIMEOUT);
HttpResources.disposeLoopsAndConnections();
System.out.println("server disposed: " + server.isDisposed());
[1] retrieve(): nothing is sent until someone subscribes
  Mono built. server hits = 0
  filter A -> GET /greet/spring
  filter B -> GET /greet/spring
  filter B <- 200
  filter A <- 200
  body = "hi, spring", server hits = 1
[2] subscribing to the same Mono again sends a second request
  filter A -> GET /greet/spring
  filter B -> GET /greet/spring
  filter B <- 200
  filter A <- 200
  body = "hi, spring", server hits = 2
[3] retrieve() on a 404
  filter A -> GET /nope
  filter B -> GET /nope
  filter B <- 404
  filter A <- 404
  NotFound, status = 404
[4] exchangeToMono() on a 404
  filter A -> GET /nope
  filter B -> GET /nope
  filter B <- 404
  filter A <- 404
  my function saw status 404
server disposed: true

Easy to get wrong

What this post leaves out

These are named and skipped for the sake of the main line. The reactive HandlerMethodArgumentResolvers that unpack the request body and arguments, and ReactiveAdapterRegistry (the registry that turns other reactive types such as RxJava or coroutines into a Publisher). Encoder/Decoder and DataBuffer, which deal with the actual bytes, and ServerCodecConfigurer, which collects them (WebFluxConfigurationSupport lines 351–356). Backpressure, which makes the sender send only as much as the receiver can take. CORS pre-flight handling (DispatcherHandler lines 211–218, AbstractHandlerMapping lines 186–218). ViewResolutionResultHandler, which renders views, and ResponseEntityResultHandler, which deals with ResponseEntity. HandlerFilterFunction, the filter for functional endpoints, and nest, which groups routes. WebSession, locale, Forwarded header handling, and the Observation integration (HttpWebHandlerAdapter lines 312–324). The setting that makes controller methods judged to be blocking subscribe on a separate scheduler (RequestMappingHandlerAdapter lines 276–279). And WebSocketHandlerAdapter leads into the topic of the next post.

Recap

Concept One-line summary
Mono / Flux A plan, not a value. It runs on subscription, and runs again on every subscription
HttpHandler The floor of WebFlux. One method that takes a request and response and returns Mono<Void>. It lives in spring-web
Server adapter Wraps the server's request and response in Spring types and calls the HttpHandler
WebHttpHandlerBuilder Wraps a WebHandler three times: filter chain, exception handling, HttpWebHandlerAdapter
WebFilter Entered in registration order, left in reverse. Takes the place of MVC's interceptors and servlet filters
WebExceptionHandler Turns an error signal into a response, from outside the filters. Without one, 500
DispatcherHandler.handle Asks the mappings in turn (concatMap, next), calls through an adapter, writes with a result handler
HandlerResultHandler The third strategy, which MVC does not have. Turns a HandlerResult into a response
HttpMessageReader / Writer The reactive counterpart of HttpMessageConverter. They pass channels, not objects
Functional endpoint RequestPredicate + HandlerFunction = RouterFunction. Registration order is the priority
RouterFunctionMapping Order -1, so it is asked before the annotation mapping
WebClient Filters (ExchangeFilterFunction) → ExchangeFunction → ClientHttpConnector
retrieve() / exchangeToMono() Turns 400 and above into an exception / your function looks at the response itself

What to look at next

We have seen MVC on servlets (Part 21) and WebFlux on reactive streams fill in the same skeleton (mapping → adapter → result) in different ways.

Everything on the web so far has been "one request, one response". The next post goes to a world where a connection stays open and both sides exchange messages. Part 25: WebSocket and Messaging — How a STOMP Message Flows follows which channels a STOMP message travels over a WebSocket connection on its way to an @MessageMapping method.

More posts