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. -
WebHandlerAPI: 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
ClientHttpConnectorcontract 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
LocaleorPrincipalfor 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
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
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,
ForwardedHeaderFilteris no longer used and has been replaced byForwardedHeaderTransformer, so forwarded headers can be processed earlier, before the exchange is created. If the filter is configured, it is removed from the filter list andForwardedHeaderTransformeris used instead.
As of version 5.1,
ForwardedHeaderFilteris deprecated and replaced byForwardedHeaderTransformerso forwarded headers can be processed earlier. If the filter is configured, it is removed from the filter list andForwardedHeaderTransformeris used instead.
1.2.3. Filters
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
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
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
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.
EncoderandDecoderencode and decode content independently of HTTP.HttpMessageReaderandHttpMessageWriterencode and decode HTTP message content.Encodercan be wrapped withEncoderHttpMessageWriter, andDecoderwithDecoderHttpMessageReader, to adapt them for use in web applications.DataBufferabstracts different byte buffer representations, such as NettyByteBufandjava.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
TokenBufferinstances, each representing a JSON object. - Each
TokenBufferis passed to Jackson’sObjectMapperto create a higher-level object. - When decoding to a single-value publisher, such as
Mono, there is oneTokenBuffer. - When decoding to a multi-value publisher, such as
Flux, eachTokenBufferis passed toObjectMapperas 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 throughObjectMapper. - For a multi-value publisher with
application/json, by default values are collected withFlux#collectToList()and the resulting collection is serialized. - For a multi-value publisher with a streaming media type such as
application/x-ndjsonorapplication/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,
Jackson2Encoderis invoked per event, and the output is flushed and delivered without delay.
By default, both
Jackson2EncoderandJackson2Decoderdo not support elements of typeString. Instead, a string or sequence of strings represents serialized JSON content that is rendered byCharSequenceEncoderby default. If you need to render a JSON array fromFlux<String>, useFlux#collectToList()and encodeMono<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
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
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
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()