Spring Web Reactive | 1. Spring WebFlux | 1.2. Reactive Core

The spring-web module includes the following basic support for reactive web applications.

  • There are two levels of support for server request handling.

    • HttpHandler: A basic contract for handling HTTP requests with non-blocking I/O and Reactive Streams back pressure, along with adapters for Servlet 3.1+ containers such as Reactor Netty, Undertow, Tomcat, and Jetty.

    • WebHandler API: A slightly higher-level, general-purpose web API for request handling, on top of which concrete programming models such as annotated controllers and functional endpoints are built.

  • On the client side, there is a basic ClientHttpConnector contract for executing HTTP requests with non-blocking I/O and Reactive Streams back pressure, plus adapters for Reactor Netty, the reactive Jetty HttpClient, and Apache HttpComponents. The higher-level WebClient used in applications is built on this contract.

  • For client and server use, codecs for serializing and deserializing HTTP request and response content.

1.2.1. HttpHandler

HttpHandler is a simple contract with a single method for handling requests and responses. It is intentionally minimal, and its only primary purpose is to provide a minimal abstraction over different HTTP server APIs.

The following table shows the supported server APIs.

Server name Server API used Reactive Streams support
Netty Netty API Reactor Netty
Undertow Undertow API spring-web: Reactive Streams bridge from Undertow
Tomcat Servlet 3.1 non-blocking I/O. Tomcat API for reading and writing ByteBuffers and byte[] spring-web: Reactive Streams bridge from Servlet 3.1 non-blocking I/O
Jetty Servlet 3.1 non-blocking I/O. Jetty API for writing ByteBuffers and byte[] spring-web: Reactive Streams bridge from Servlet 3.1 non-blocking I/O
Servlet 3.1 container Servlet 3.1 non-blocking I/O spring-web: Reactive Streams bridge from Servlet 3.1 non-blocking I/O

The following table shows the server dependencies. See supported versions.

Server name Group Id Artifact name
Reactor Netty io.projectreactor.netty reactor-netty
Undertow io.undertow undertow-core
Tomcat org.apache.tomcat.embed tomcat-embed-core
Jetty org.eclipse.jetty jetty-server, jetty-servlet

The following code snippets show how to use HttpHandler adapters with each server API.

Reactor Netty

Java

HttpHandler handler = ...
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
HttpServer.create().host(host).port(port).handle(adapter).bind().block();

Kotin

val handler: HttpHandler = ...
val adapter = ReactorHttpHandlerAdapter(handler)
HttpServer.create().host(host).port(port).handle(adapter).bind().block()

Tomcat

Java

HttpHandler handler = ...
Servlet servlet = new TomcatHttpHandlerAdapter(handler);

Tomcat server = new Tomcat();
File base = new File(System.getProperty("java.io.tmpdir"));
Context rootContext = server.addContext("", base.getAbsolutePath());
Tomcat.addServlet(rootContext, "main", servlet);
rootContext.addServletMappingDecoded("/", "main");
server.setHost(host);
server.setPort(port);
server.start();

Kotin

val handler: HttpHandler = ...
val servlet = TomcatHttpHandlerAdapter(handler)

val server = Tomcat()
val base = File(System.getProperty("java.io.tmpdir"))
val rootContext = server.addContext("", base.absolutePath)
Tomcat.addServlet(rootContext, "main", servlet)
rootContext.addServletMappingDecoded("/", "main")
server.host = host
server.setPort(port)
server.start()

Jetty

Java

HttpHandler handler = ...
Servlet servlet = new JettyHttpHandlerAdapter(handler);

Server server = new Server();
ServletContextHandler contextHandler = new ServletContextHandler(server, "");
contextHandler.addServlet(new ServletHolder(servlet), "/");
contextHandler.start();

ServerConnector connector = new ServerConnector(server);
connector.setHost(host);
connector.setPort(port);
server.addConnector(connector);
server.start();

Kotin

val handler: HttpHandler = ...
val servlet = JettyHttpHandlerAdapter(handler)

val server = Server()
val contextHandler = ServletContextHandler(server, "")
contextHandler.addServlet(ServletHolder(servlet), "/")
contextHandler.start();

val connector = ServerConnector(server)
connector.host = host
connector.port = port
server.addConnector(connector)
server.start()

Servlet 3.1+ Container

To deploy as a WAR to a Servlet 3.1+ container, you can extend AbstractReactiveWebInitializer and include it in the WAR. That class wraps an HttpHandler in a ServletHttpHandlerAdapter and registers it as a Servlet.

1.2.2. WebHandler API

The org.springframework.web.server package provides a general-purpose web API for processing requests through a chain of multiple WebExceptionHandler, multiple WebFilter, and a single WebHandler component, based on HttpHandler. You can assemble it with WebHttpHandlerBuilder by specifying a Spring ApplicationContext that automatically detects chain components, or by registering components with the builder.

While HttpHandler is intended to abstract the use of different HTTP servers, the WebHandler API is intended to provide a broader set of features commonly used in web applications, such as the following.

  • User sessions with attributes.
  • Request attributes.
  • Resolved Locale or Principal for the request.
  • Access to parsed and cached form data.
  • Multipart data abstractions.
  • And so on.

Special Bean Types

The following table lists components that WebHttpHandlerBuilder can automatically detect from the Spring ApplicationContext, or that can be registered directly.

Bean name Bean type Count Description
<any> WebExceptionHandler 0..N Provides exception handling from the chain of WebFilter instances and the target WebHandler. See Exceptions for details.
<any> WebFilter 0..N Applies interception-style logic before and after the rest of the filter chain and the target WebHandler. See Filters for details.
webHandler WebHandler 1 Request handler.
webSessionManager WebSessionManager 0..1 Manager for WebSession instances exposed through methods on ServerWebExchange. The default is DefaultWebSessionManager.
serverCodecConfigurer ServerCodecConfigurer 0..1 Access to HttpMessageReader instances for parsing form data and multipart data. This data is exposed through methods on ServerWebExchange. The default is ServerCodecConfigurer.create().
localeContextResolver LocaleContextResolver 0..1 Resolver for the LocaleContext exposed through methods on ServerWebExchange. The default is AcceptHeaderLocaleContextResolver.
forwardedHeaderTransformer ForwardedHeaderTransformer 0..1 For processing forwarded-type headers by extracting and removing them, or by discarding them. It is not used by default.

Form Data

ServerWebExchange provides the following method for accessing form data.

Java

Mono<MultiValueMap<String, String>> getFormData();

Kotin

suspend fun getFormData(): MultiValueMap<String, String>

DefaultServerWebExchange uses a configured HttpMessageReader to parse form data (application/x-www-form-urlencoded) into a MultiValueMap. By default, FormHttpMessageReader is configured through the ServerCodecConfigurer Bean. See WebHandler API.

Multipart Data

Web MVC

ServerWebExchange provides the following method for accessing multipart data.

Java

Mono<MultiValueMap<String, Part>> getMultipartData();

Kotin

suspend fun getMultipartData(): MultiValueMap<String, Part>

DefaultServerWebExchange uses a configured HttpMessageReader<MultiValueMap<String, Part>> to parse multipart/form-data content into a MultiValueMap. The default is DefaultPartHttpMessageReader, which has no third-party dependency. Alternatively, you can use SynchronossPartHttpMessageReader, based on the Synchronoss NIO Multipart library. Both are configured through the ServerCodecConfigurer Bean. See WebHandler API.

To parse multipart data in streaming form, use the Flux<Part> returned from HttpMessageReader<Part>. For example, using @RequestPart in an annotated controller means accessing each part by name, as with a Map, so the multipart data must be fully parsed. By contrast, with @RequestBody, content can be decoded to Flux<Part> without collecting it into a MultiValueMap.

Forwarded Headers

Web MVC

When a request goes through a proxy, such as a load balancer, the host, port, and scheme may change. As a result, it can be difficult to create links that point to the correct host, port, and scheme from the client’s perspective.

RFC 7239 defines Forwarded HTTP headers that proxies can use to provide information about the original request. There are also other non-standard headers, such as X-Forwarded-Host, X-Forwarded-Port, X-Forwarded-Proto, X-Forwarded-Ssl, and X-Forwarded-Prefix.

ForwardedHeaderTransformer is a component that modifies the request host, port, and scheme based on forwarded headers and then removes those headers. If declared as a Bean named forwardedHeaderTransformer, it is detected automatically and used.

Forwarded headers involve security considerations because the application cannot know whether the headers were added by a proxy as intended or by a malicious client. This is why a proxy at the trust boundary should be configured to remove untrusted forwarded traffic from the outside. You can configure ForwardedHeaderTransformer with removeOnly=true. In that case, the headers are removed and not used.

As of version 5.1, ForwardedHeaderFilter is no longer used and has been replaced by ForwardedHeaderTransformer, so forwarded headers can be processed earlier, before the exchange is created. If the filter is configured, it is removed from the filter list and ForwardedHeaderTransformer is used instead.

As of version 5.1, ForwardedHeaderFilter is deprecated and replaced by ForwardedHeaderTransformer so forwarded headers can be processed earlier. If the filter is configured, it is removed from the filter list and ForwardedHeaderTransformer is used instead.

1.2.3. Filters

Web MVC

The WebHandler API uses WebFilter to apply interception logic before and after the rest of the filter chain and the target WebHandler. When using WebFlux configuration, WebFilter registration is as simple as declaring it as a Spring Bean, and optionally using @Order on the Bean declaration or implementing Ordered to set priority.

CORS

Web MVC

Spring WebFlux provides detailed support for CORS configuration through controller annotations. However, when used with Spring Security, it is better to use the built-in CorsFilter, which must be ordered before the Spring Security filter chain.

For details, see CORS and the webflux-cors.html section.

1.2.4. Exceptions

Web MVC

The WebHandler API can use WebExceptionHandler to handle exceptions from the chain of WebFilter instances and the target WebHandler. When using WebFlux configuration, registering a WebExceptionHandler is as simple as declaring it as a Spring Bean, and optionally using @Order on the Bean declaration or implementing Ordered to set priority.

The following table describes the available WebExceptionHandler implementations.

Exception handler Description
ResponseStatusExceptionHandler Provides exception handling for ResponseStatusException by setting the response to the HTTP status code of the exception.
WebFluxResponseStatusExceptionHandler An extension of ResponseStatusExceptionHandler that can also determine the HTTP status code from an @ResponseStatus annotation. This handler can be declared in WebFlux configuration.

1.2.5. Codecs

Web MVC

The spring-web and spring-core modules provide support for serializing and deserializing byte content to and from higher-level objects through non-blocking I/O with Reactive Streams back pressure. This support is described below.

  • Encoder and Decoder encode and decode content independently of HTTP.
  • HttpMessageReader and HttpMessageWriter encode and decode HTTP message content.
  • Encoder can be wrapped with EncoderHttpMessageWriter, and Decoder with DecoderHttpMessageReader, to adapt them for use in web applications.
  • DataBuffer abstracts different byte buffer representations, such as Netty ByteBuf and java.nio.ByteBuffer, and is what all codecs work on. For more on this topic, see Data Buffers and Codecs in the Spring Core section.

The spring-core module provides implementations of encoders and decoders for byte[], ByteBuffer, DataBuffer, Resource, and String. The spring-web module provides Jackson JSON, Jackson Smile, JAXB2, Protocol Buffers, and other encoders and decoders, as well as web-specific HTTP message Reader and Writer implementations for form data, multipart content, server-sent events, and more.

ClientCodecConfigurer and ServerCodecConfigurer are typically used to configure and customize codecs used by applications. See the HTTP message codecs configuration section.

Jackson JSON

JSON and binary JSON (Smile) are supported when the Jackson library is present.

Jackson2Decoder works as follows.

  • Jackson’s asynchronous, non-blocking parser is used to aggregate a stream of byte chunks into TokenBuffer instances, each representing a JSON object.
  • Each TokenBuffer is passed to Jackson’s ObjectMapper to create a higher-level object.
  • When decoding to a single-value publisher, such as Mono, there is one TokenBuffer.
  • When decoding to a multi-value publisher, such as Flux, each TokenBuffer is passed to ObjectMapper as soon as enough bytes have been received for a fully formed object. The input content can be a JSON array or a line-delimited JSON format such as NDJSON, JSON Lines, or a JSON Text Sequence.

Jackson2Encoder works as follows.

  • For a single-value publisher, such as Mono, it is simply serialized through ObjectMapper.
  • For a multi-value publisher with application/json, by default values are collected with Flux#collectToList() and the resulting collection is serialized.
  • For a multi-value publisher with a streaming media type such as application/x-ndjson or application/stream+x-jackson-smile, values are encoded, written, and flushed individually by using a line-delimited JSON format. Other streaming media types can be registered with the encoder.
  • For SSE, Jackson2Encoder is invoked per event, and the output is flushed and delivered without delay.

By default, both Jackson2Encoder and Jackson2Decoder do not support elements of type String. Instead, a string or sequence of strings represents serialized JSON content that is rendered by CharSequenceEncoder by default. If you need to render a JSON array from Flux<String>, use Flux#collectToList() and encode Mono<List<String>>.

Form Data

FormHttpMessageReader and FormHttpMessageWriter support decoding and encoding application/x-www-form-urlencoded content.

On the server side, where form content often needs to be accessed from multiple places, ServerWebExchange provides a dedicated getFormData() method that parses content through FormHttpMessageReader and caches the result for repeated use. See Form Data in the WebHandler API section.

After getFormData() is used, the original content can no longer be read from the request body. Therefore, instead of reading from the raw request body, applications must go through ServerWebExchange to access the cached form data.

Multipart

MultipartHttpMessageReader and MultipartHttpMessageWriter support decoding and encoding multipart/form-data content. MultipartHttpMessageReader delegates to another HttpMessageReader to parse into Flux<Part> and collect the result into a MultiValueMap. By default, DefaultPartHttpMessageReader is used, but this can be changed through ServerCodecConfigurer. For details on DefaultPartHttpMessageReader, see the javadoc for DefaultPartHttpMessageReader.

On the server side, where multipart form content may be accessed from multiple places, ServerWebExchange provides a dedicated getMultipartData() method that parses content through MultipartHttpMessageReader and caches the result for repeated use. See Multipart Data in the WebHandler API section.

After getMultipartData() is used, the original raw content can no longer be read from the request body. Therefore, applications must consistently use getMultipartData() for repeated map-like access to parts, or rely on SynchronossPartHttpMessageReader for one-time access to Flux<Part>.

Limits

Decoder and HttpMessageReader implementations that buffer part or all of an input stream can set a limit on the maximum number of bytes to buffer in memory. Buffering can occur because the input is represented as one aggregated object, for example a controller method with @RequestBody byte[] or x-www-form-urlencoded data. It can also occur in streaming when the buffered input stream is split, for example delimited text or a stream of JSON objects. In such streaming cases, the limit applies to the bytes associated with one object in the stream.

To configure buffer sizes, check whether a given Decoder or HttpMessageReader exposes a maxInMemorySize property, and if so, check its Javadoc for default value details. On the server side, ServerCodecConfigurer provides a single place to configure all codecs. See HTTP message codecs. On the client side, limits for all codecs can be changed from WebClient.Builder.

For multipart parsing, the maxInMemorySize property limits the size of non-file parts. For file parts, it determines the threshold at which the part is written to disk. For file parts written to disk, there is an additional maxDiskUsagePerPart property that limits the amount of disk space per part. There is also a maxParts property that limits the number of parts in a multipart request. To configure all three in WebFlux, you must provide an instance of ServerCodecConfigurer with a configured MultipartHttpMessageReader.

Streaming

Web MVC

When streaming to an HTTP response, for example text/event-stream or application/x-ndjson, it is important to send data periodically in order to detect disconnected clients as quickly as possible. Such sends can be a short string, an empty SSE event, or other “no-op” data that effectively functions as a heartbeat.

DataBuffer

DataBuffer is WebFlux’s representation of a byte buffer. The Spring Core part of this reference describes it in detail in the Data Buffers and Codecs section. The important point to understand is that on some servers, such as Netty, byte buffers are pooled and reference-counted, and must be consumed and released to avoid memory leaks.

WebFlux applications generally do not need to consider these issues unless they directly consume or produce data buffers without converting to and from higher-level objects through codecs, or unless they choose to create custom codecs. In those cases, review the information in Data Buffers and Codecs, especially the section on DataBuffer usage.

1.2.6. Logging

Web MVC

In Spring WebFlux, DEBUG-level logging is designed to be compact, minimal, and human-friendly. It focuses on information that is consistently useful, as opposed to other information that is useful only when debugging a specific issue.

TRACE-level logging generally follows the same principles as DEBUG, for example it should not become a firehose, but it can be used for debugging issues. Some log messages may also have different levels of detail between TRACE and DEBUG.

Good logging comes from experience using logs. If you find anything that does not match the stated purpose, please let us know.

Log ID

WebFlux can execute a single request across multiple threads. Thread IDs are not very helpful for correlating log messages that belong to a specific request. Therefore, by default, WebFlux log messages are prefixed with a request-specific ID.

On the server side, the log ID is stored in a ServerWebExchange attribute (LOG_ID_ATTRIBUTE), while the fully formatted prefix based on that ID is available from ServerWebExchange#getLogPrefix(). In WebClient, the log ID is stored in a ClientRequest attribute (LOG_ID_ATTRIBUTE), while the fully formatted prefix is available from ClientRequest#logPrefix().

Sensitive Data

Web MVC

DEBUG and TRACE logging can record confidential information. Therefore, form parameters and headers are masked by default, and logging for them must be enabled explicitly.

The following example shows how to do this for server-side requests.

Java

@Configuration
@EnableWebFlux
class MyConfig implements WebFluxConfigurer {

    @Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
        configurer.defaultCodecs().enableLoggingRequestDetails(true);
    }
}

Kotlin

@Configuration
@EnableWebFlux
class MyConfig : WebFluxConfigurer {

    override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
        configurer.defaultCodecs().enableLoggingRequestDetails(true)
    }
}

The following example shows how to do this for client requests.

Java

Consumer<ClientCodecConfigurer> consumer = configurer ->
        configurer.defaultCodecs().enableLoggingRequestDetails(true);

WebClient webClient = WebClient.builder()
        .exchangeStrategies(strategies -> strategies.codecs(consumer))
        .build();

Kotlin

val consumer: (ClientCodecConfigurer) -> Unit  = { configurer -> configurer.defaultCodecs().enableLoggingRequestDetails(true) }

val webClient = WebClient.builder()
        .exchangeStrategies({ strategies -> strategies.codecs(consumer) })
        .build()

Appenders

Logging libraries such as SLF4J and Log4J2 provide asynchronous loggers to avoid blocking. They have their own drawbacks, such as dropping messages that cannot be queued for logging, but they are currently the best available choice for reactive, non-blocking applications.

Custom Codecs

Applications can register custom codecs to support additional media types, or specific behavior not supported by the default codecs.

Some configuration options exposed to developers apply to the default codecs. Custom codecs may want the opportunity to align with those settings, such as enforcing buffering limits or logging confidential data.

The following example shows how to configure a custom codec for client-side requests.

Java

WebClient webClient = WebClient.builder()
        .codecs(configurer -> {
                CustomDecoder decoder = new CustomDecoder();
                configurer.customCodecs().registerWithDefaultConfig(decoder);
        })
        .build();

Kotlin

val webClient = WebClient.builder()
        .codecs({ configurer ->
                val decoder = CustomDecoder()
                configurer.customCodecs().registerWithDefaultConfig(decoder)
         })
        .build()