If you create your project using the Micronaut CLI create-app command, the http-server dependency is included by default.
Micronaut framework includes both non-blocking HTTP server and client APIs based on Netty.
The goal of the HTTP server is to make it as easy as possible to expose services to be consumed by HTTP clients and web pages, regardless of the language they are written in. There is support for rendering HTML from a variety of templating frameworks in the Micronaut Views module. To use the HTTP server you need the http-server-netty dependency in your build:
To run the application from a unit test, use the EmbeddedServer interface:
Micronaut Test Case
Important
Without explicit port configuration, the port will be 8080, unless the application is run under the test environment where the port is random. When the application context starts from the context of a test class, the test environment is added automatically.
3 Running Server on a Specific Port
By default, the server runs on port 8080. However, you can set the server to run on a specific port:
micronaut.server.port=8086
micronaut: server: port: 8086
[micronaut.server]port = 8086
micronaut { server { port = 8086 }}
{micronaut = {server = {port = 8086 } }}
{ "micronaut": { "server": { "port": 8086 } }}
Tip
This is also configurable from an environment variable, e.g. MICRONAUT_SERVER_PORT=8086
To run on a random port:
micronaut.server.port=-1
micronaut: server: port: -1
[micronaut.server]port = -1
micronaut { server { port = -1 }}
{micronaut = {server = {port = -1 } }}
{ "micronaut": { "server": { "port": -1 } }}
Tip
Setting an explicit port may cause tests to fail if multiple servers start simultaneously on the same port. To prevent that, specify a random port in the test environment configuration.
4 HTTP Routing
The @Controller annotation used in the previous section is one of several annotations that allow you to control the construction of HTTP routes.
URI Paths
The value of the @Controller annotation is an RFC-6570 URI template, so you can embed URI variables within the path using the syntax defined by the URI template specification.
Note
Many other frameworks, including Spring, implement the URI template specification
You can use this class in your applications to build URIs, for example:
Using a UriTemplate
You can use UriTemplate to build paths to include in your responses.
URI Path Variables
URI variables can be referenced via method arguments. When the path variable matches the method argument name, they are bound together automatically. If you want to use different names or specify a default value for a missing URI Variable, the PathVariable annotation can be used. The following example illustrates these options:
URI Variables Example
The Micronaut framework maps the URI /issues/{number} for the above controller. We can assert this is the case by writing unit tests:
Testing URI Variables
Note that the URI template in the previous example requires that the number variable is specified. You can specify optional URI templates with the syntax: /issues{/number} and by annotating the number parameter with @Nullable. Alternatively, you can use the defaultValue element of the PathVariable annotation to specify a default value when the URI variable is missing. For example:
Default Value Example
Testing Default Value
Tip
To read all the variables of the route with one argument, see Path Variables.
The following table provides examples of URI templates and what they match:
Table 1. URI Template Matching
Template
Description
Matching URI
/books/{id}
Simple match
/books/1
/books/{id:2}
A variable of two characters max
/books/10
/books{/id}
An optional URI variable
/books/10 or /books
/book{/id:[a-zA-Z]+}
An optional URI variable with regex
/books/foo
/books{?max,offset}
Optional query parameters
/books?max=10&offset=10
/books{/path:.*}{.ext}
Regex path match with extension
/books/foo/bar.xml
URI Reserved Character Matching
By default, URI variables as defined by the RFC-6570 URI template spec cannot include reserved characters such as /, ? etc.
This can be problematic if you wish to match or expand entire paths. As per section 3.2.3 of the specification, you can use reserved expansion or matching using the + operator.
For example the URI /books/{+path} matches both /books/foo and /books/foo/bar since the + indicates that the variable path should include reserved characters (in this case /).
Routing Annotations
The previous example uses the @Get annotation to add a method that accepts HTTP GET requests. The following table summarizes the available annotations and how they map to HTTP methods:
The RouteCondition annotation allows you to define a condition that must evaluate to true for a route to match a request. The condition is specified as a Micronaut Expression Language (EL) expression and is evaluated at request time.
Within the expression, a request variable is available that references the current HttpRequest.
This is useful for routing requests based on query parameters, headers, or any other aspect of the request. For example, you can use @RouteCondition to route requests to different methods depending on the value of a query parameter:
Route Condition Example
Note
The RouteCondition annotation only applies to server-side routes and is ignored when placed on declarative HTTP client routes.
Tip
The request variable in the expression is an instance of HttpRequest. You can access headers via request.headers, query parameters via request.parameters, and other request attributes as needed.
@Options
CORS support handles OPTIONS preflight requests. However, if you want to dispatch OPTIONS requests without an Origin HTTP Header, you can enable it via:
Each of the routing annotations supports multiple URI templates. For each template, a route is created. This feature is useful for example to change the path of the API and leave the existing path as is for backwards compatibility. For example:
Multiple URIs
Note
Route validation is more complicated with multiple templates. If a variable that would normally be required does not exist in all templates, that variable is considered optional since it may not exist for every execution of the method.
Building Routes Programmatically
If you prefer to not use annotations and instead declare all routes in code then never fear, the Micronaut framework has a flexible RouteBuilder API that makes it a breeze to define routes programmatically.
To start, subclass DefaultRouteBuilder and inject the controller to route to into the method, and define your routes:
Use @Inject to inject a method with the controller to route to
Use methods such as RouteBuilder::GET(String,Class,String,Class…) to route to controller methods. Note that even though the issues controller is used, the route has no knowledge of its @Controller annotation and thus the full path must be specified.
Note
A route given an object, like the injected controller above, targets the bean of the type of the object, not the object itself: when the route is built, the bean definition of that type is looked up, and the method is invoked on that bean. The type must therefore resolve to a single bean. To target a specific bean definition, for example one of several @EachProperty beans, use the variants that take a BeanDefinition and an ExecutableMethod, such as RouteBuilder::GET(String,BeanDefinition,ExecutableMethod).
Tip
Unfortunately due to type erasure, a Java method lambda reference cannot be used with the API. For Groovy there is a GroovyRouteBuilder class which can be subclassed that allows passing Groovy method references.
Python applications declare routes as module-level functions instead of subclassing a route builder. See Module Scripts for how a module of route functions is turned into a controller.
Route Compile-Time Validation
The Micronaut framework supports validating route arguments at compile time with the validation library. To get started, add the micronaut-http-validation dependency to your build:
With the correct dependency on your classpath, route arguments will automatically be checked at compile time. Compilation will fail if any of the following conditions are met:
The URI template contains a variable that is optional, but the method parameter is not annotated with @Nullable or is an java.util.Optional.
An optional variable is one that allows the route to match a URI even if the value is not present. For example /foo{/bar} matches requests to /foo and /foo/abc. The non-optional variant would be /foo/{bar}. See the URI Path Variables section for more information.
The URI template contains a variable that is missing from the method arguments.
Note
To disable route compile-time validation, set the system property -Dmicronaut.route.validation=false. For Java and Kotlin users using Gradle, the same effect can be achieved by removing the micronaut-http-validation dependency from the annotationProcessor/kapt scope.
Routing non-standard HTTP methods
The @CustomHttpMethod annotation supports non-standard HTTP methods for a client or server. Specifications like RFC-4918 Webdav require additional methods like REPORT or LOCK for example.
RoutingExample
@CustomHttpMethod(method = "LOCK", value = "/{name}")String lock(String name)
The annotation can be used anywhere the standard method annotations can be used, including controllers and declarative HTTP clients.
RouteMatch
The RouteMatch API provides information about an executable Route.
Given a request you can retrieve a RouteMatch with:
The examples in the previous section demonstrate how the Micronaut framework lets you bind method parameters from URI path variables. This section shows how to bind arguments from other parts of the request.
Binding Annotations
All binding annotations support customization of the name of the variable being bound from with their name member.
The following table summarizes the annotations and their purpose, and provides examples:
The method parameter name is used when a value is not specified in a binding annotation. In other words the following two methods are equivalent and both bind from a cookie named myCookie:
The Micronaut framework also supports binding the body to an InputStream. If the method is reading the stream, the method execution must be offloaded to another thread pool to avoid blocking the event loop.
Performing Blocking I/O With InputStream
Binding from Multiple Query values
Instead of binding from a single section of the request, it may be desirable to bind all query values for example to a POJO. This can be achieved by using the exploded operator (?pojo*) in the URI template. For example:
@Controller("/api")open class BookmarkController { @Get("/bookmarks/list{?paginationCommand*}") open fun list(@Valid paginationCommand: PaginationCommand): HttpStatus { return HttpStatus.OK }}
Instead of binding just query values, it is also possible to bind any Bindable value to a POJO (e.g. to bind HttpRequest, @PathVariable, @QueryValue and @Header to a single POJO). This can be achieved with the @RequestBean annotation and a custom Bean class with fields with Bindable annotations, or fields that can be bound by type (e.g. HttpRequest, BasicAuth, Authentication, etc).
For example:
Binding Bindable values to POJO
@Controller("/api")public class MovieTicketController { // You can also omit query parameters like: // @Get("/movie/ticket/{movieId} @Get("/movie/ticket/{movieId}{?minPrice,maxPrice}") public HttpStatus list(@Valid @RequestBean MovieTicketBean bean) { return HttpStatus.OK; }}
@Controller("/api")class MovieTicketController: # You can also omit query parameters like: # @Get("/movie/ticket/{movieId} @Get("/movie/ticket/{movieId}{?minPrice,maxPrice}") def list(self, bean: Annotated[MovieTicketBean, Valid, RequestBean]) -> HttpStatus: return HttpStatus.OK
@Controller("/api")open class MovieTicketController { // You can also omit query parameters like: // @Get("/movie/ticket/{movieId} @Get("/movie/ticket/{movieId}{?minPrice,maxPrice}") open fun list(@Valid @RequestBean bean: MovieTicketBean): HttpStatus { return HttpStatus.OK }}
@Controller("/api")class MovieTicketController { // You can also omit query parameters like: // @Get("/movie/ticket/{movieId} @Get("/movie/ticket/{movieId}{?minPrice,maxPrice}") HttpStatus list(@Valid @RequestBean MovieTicketBean bean) { HttpStatus.OK }}
@Introspecteddata class MovieTicketBean( val httpRequest: HttpRequest<Any>, @field:PathVariable val movieId: String, @field:QueryValue @field:PositiveOrZero @field:Nullable val minPrice: Double, @field:QueryValue @field:PositiveOrZero @field:Nullable val maxPrice: Double)
The bean class has to be introspected with @Introspected. It can be one of:
Mutable Bean class with setters and getters
Immutable Bean class with getters and an all-argument constructor (or @Creator annotation on a constructor or static method). Arguments of the constructor must match field names so the object can be instantiated without reflection.
Warning
Since Java does not retain argument names in bytecode, you must compile code with -parameters to use an immutable bean class from another jar. Another option is to extend Bean class in your source.
Bindable Types
Generally any type that can be converted from a String representation to a Java type via the ConversionService API can be bound to.
This includes most common Java types. However, you can simply add additional TypeConverter either by defining it as a bean or by registering it in a TypeConverterRegistrar via the service loader.
The handling of nullability deserves special mention. Consider for example the following example:
In this case, if the HTTP header Content-Type is not present in the request, the route is considered invalid, since it cannot be satisfied, and an HTTP 400 BAD REQUEST is returned.
To make the Content-Type header optional, you can instead write:
A null string is passed if the header is absent from the request.
Note
java.util.Optional can also be used, but that is discouraged for method parameters.
Additionally, any DateTime that conforms to RFC-1123 can be bound to a parameter. Alternatively the format can be customized with the Format annotation:
Some parameters are recognized by their type instead of their annotation. The following table summarizes the parameter types, their purpose, and provides an example:
The Micronaut framework tries to populate method arguments in the following order:
URI variables like /{id}.
From query parameters if the request is a GET request (e.g. ?foo=bar).
If there is a @Body and request allows the body, bind the body to it.
If the request can have a body and no @Body is defined then try to parse the body (either JSON or form data) and bind the method arguments from the body (see the example).
Finally, if the method arguments cannot be populated return 400 BAD REQUEST.
Binding Method Arguments From Body with no @Body
7 Custom Argument Binding
The Micronaut framework uses an ArgumentBinderRegistry to look up ArgumentBinder beans
capable of binding to the arguments in controller methods. The default implementation looks for an annotation on the argument that is meta-annotated with @Bindable. If one exists the argument binder registry searches for an argument binder that supports that annotation.
If no fitting annotation is found, the Micronaut framework tries to find an argument binder that supports the argument type.
An argument binder returns a ArgumentBinder.BindingResult. The binding result gives the Micronaut framework more information than just the value. Binding results are either satisfied or unsatisfied, and either empty or not empty. If an argument binder returns an unsatisfied result, the binder may be called again at different times in request processing. Argument binders are initially called before the body is read and before any filters are executed. If a binder relies on any of that data, and it is not present, return a ArgumentBinder.BindingResult#UNSATISFIED result. Returning an ArgumentBinder.BindingResult#EMPTY or satisfied result will be the final result and the binder will not be called again for that request.
Argument binders that bind based on the presence of an annotation must implement AnnotatedRequestArgumentBinder, and can be used by creating an annotation that is annotated with Bindable. For example:
An example of a binding annotation
Example of annotated data binding
Tip
It is common to use ConversionService to convert the data to the type of the argument.
Once the binder is created, we can annotate an argument in our controller method which will be bound using the custom logic we’ve specified.
A controller operation with this annotated binding
TypedRequestArgumentBinder
Argument binders that bind based on the type of the argument must implement TypedRequestArgumentBinder. For example, given this class:
Example of POJO
imports
import io.micronaut.core.annotation.Introspected;
@Introspectedpublic class ShoppingCart { private String sessionId; private Integer total; public String getSessionId() { return sessionId; } public void setSessionId(String sessionId) { this.sessionId = sessionId; } public Integer getTotal() { return total; } public void setTotal(Integer total) { this.total = total; }}
We can define a TypedRequestArgumentBinder for this class, as seen below:
Example of typed data binding
Once the binder is created, it is used for any controller argument of the associated type:
A controller operation with this typed binding
8 Host Resolution
You may need to resolve the host name of the current server. The Micronaut framework includes an implementation of the HttpHostResolver interface.
The default implementation looks for host information in the following places in order:
The supplied configuration
The Forwarded header
X-Forwarded- headers. If the X-Forwarded-Host header is not present, the other X-Forwarded headers are ignored.
The Host header
The properties on the request URI
The properties on the embedded server URI
The behavior of which headers to pull the relevant data can be changed with the following configuration:
The above configuration also supports an allowed host list. Configuring this list ensures any resolved host matches one of the supplied regular expression patterns. That is useful to prevent host cache poisoning attacks and is recommended to be configured.
9 Locale Resolution
The Micronaut framework supports several strategies for resolving locales for a given request. The getLocale-- method is available on the request, however it only supports parsing the Accept-Language header. For other use cases where the locale can be in a cookie, the user’s session, or should be set to a fixed value, HttpLocaleResolver can be used to determine the current locale.
The LocaleResolver API does not need to be used directly. Simply define a parameter to a controller method of type java.util.Locale and the locale will be resolved and injected automatically.
There are several configuration options to control how to resolve the locale:
Locales can be configured in the "en_GB" format, or in the BCP 47 (Language tag) format. If multiple methods are configured, the fixed locale takes precedence, followed by session/cookie, then header.
If any of the built-in methods do not meet your use case, create a bean of type HttpLocaleResolver and set its order (through the getOrder method) relative to the existing resolvers.
10 Client IP Address
You may need to resolve the originating IP address of an HTTP Request. The Micronaut framework includes an implementation of HttpClientAddressResolver.
The default implementation resolves the client address in the following places in order:
The configured header
The Forwarded header
The X-Forwarded-For header
The remote address on the request
The first priority header name can be configured with micronaut.server.client-address-header.
11 The HttpRequest and HttpResponse
If you need more control over request processing you can write a method that receives the complete HttpRequest.
In fact, there are several higher-level interfaces that can be bound to controller method parameters. These include:
The HttpRequest should be declared parametrized with a concrete generic type if the request body is needed, e.g. HttpRequest<MyClass> request. The body may not be available from the request otherwise.
In addition, for full control over the emitted HTTP response you can use the static factory methods of the HttpResponse class which return a MutableHttpResponse.
The following example implements the previous MessageController example using the HttpRequest and HttpResponse objects:
Generally ServerRequestContext is available within reactive flow, but the recommended approach is consumed the request as an argument as shown in the previous example. If the request is needed in downstream methods it should be passed as an argument to those methods. There are cases where the context is not propagated because other threads are used to emit the data.
An alternative for users of Project Reactor to using the ServerRequestContext is to use the contextual features of Project Reactor to retrieve the request. Because the Micronaut Framework uses Project Reactor as it’s default reactive streams implementation, users of Project Reactor can benefit by being able to access the request in the context. For example:
Using the Project Reactor context
Using the context to retrieve the request is the best approach for reactive flows because Project Reactor propagates the context, and it does not rely on a thread local like ServerRequestContext.
12 Response Status
A Micronaut controller action responds with a 200 HTTP status code by default.
If the action returns an HttpResponse, configure the status code for the response with the status method.
Since 5.3.0, an action returning CompletionStage<HttpStatus> treats the completed status as a deliberate response with no body. For example, CompletableFuture.completedFuture(HttpStatus.ACCEPTED) answers 202 Accepted, including when the stage completes later. Previously, it answered 404 Not Found with the default missing-body policy, or 204 No Content when micronaut.server.not-found-on-missing-body was false. An applicable @Status annotation still takes precedence for the asynchronous result.
To send a Content-Disposition response header, use the @ContentDisposition annotation. This is independent of the return type of the method, so it works with a plain object, a reactive type, or anything else the route returns.
The example above results in a response with the header Content-Disposition: attachment; filename="report.csv"; filename*=utf-8''report.csv, prompting the browser to download the response as a file.
To have the browser display the response instead of downloading it, set type to ContentDisposition.Type.INLINE:
@ContentDisposition(type = ContentDisposition.Type.INLINE)@Get(value = "/preview", produces = MediaType.TEXT_PLAIN)public String preview() { return "This is displayed rather than downloaded";}
@ContentDisposition(type=ContentDispositionType.INLINE)@Get(value="/preview", produces=MediaType.TEXT_PLAIN)def preview(self) -> str: return "This is displayed rather than downloaded"
@ContentDisposition(type = ContentDisposition.Type.INLINE)@Get(value = "/preview", produces = [MediaType.TEXT_PLAIN])fun preview(): String { return "This is displayed rather than downloaded"}
@ContentDisposition(type = ContentDisposition.Type.INLINE)@Get(value = "/preview", produces = MediaType.TEXT_PLAIN)String preview() { "This is displayed rather than downloaded"}
14 Response Content-Type
A Micronaut controller action produces application/json by default. However, you can change the Content-Type of the response with the @Produces annotation or the produces member of the HTTP method annotations.
15 Accepted Request Content-Type
A Micronaut controller action consumes application/json by default. Consuming other content types is supported with the @Consumes annotation, or the consumes member of any HTTP method annotation.
Customizing Processed Content Types
Normally JSON parsing only happens if the content type is application/json. The other MediaTypeCodec classes behave similarly in that they have predefined content types they can process. To extend the list of media types that a given codec processes, provide configuration that will be stored in CodecConfiguration:
The currently supported configuration prefixes are json, json-stream, text, and text-stream.
16 Reactive HTTP Request Processing
As mentioned previously, Micronaut framework is built on Netty which is designed around an Event loop model and non-blocking I/O. Micronaut executes code defined in @Controller beans in the same thread as the request thread (an Event Loop thread).
This makes it critical that if you do any blocking I/O operations (for example interactions with Hibernate/JPA or JDBC) that you offload those tasks to a separate thread pool that does not block the Event loop.
For example the following configuration configures the I/O thread pool as a fixed thread pool with 75 threads (similar to what a traditional blocking server such as Tomcat uses in the thread-per-request model):
To use this thread pool in a @Controller bean you have a number of options. The simplest is to use the @ExecuteOn annotation, which can be applied only to either a @Controller or @Filter at the type or method level. It indicates the configured thread pool to run method(s) of the controller or filter on:
Using @ExecuteOn
The value of the @ExecuteOn annotation can be any named executor defined under micronaut.executors.
Tip
Generally speaking for database operations you want a thread pool configured that matches the maximum number of connections specified in the database connection pool.
For a Kotlin suspend function, the selected executor is also the dispatcher the coroutine resumes on, so work after a suspension point stays on that executor rather than falling back to Dispatchers.Default. Child coroutines inherit it, and an explicit withContext still wins for the duration of its block.
Note
An explicit @ExecuteOn and the micronaut.server.thread-selection setting both resolve through the same executor selection, so a suspend route is offloaded under AUTO, IO and BLOCKING thread selection just as a blocking route is. To keep a suspend route on the event loop under AUTO, annotate it with @NonBlocking.
An alternative to the @ExecuteOn annotation is to use the facility provided by the reactive library you have chosen. Reactive implementations such as Project Reactor or RxJava feature a subscribeOn method which lets you alter which thread executes user code. For example:
Reactive subscribeOn Example
16.1 Using the @Body Annotation
To parse the request body, you first indicate to the Micronaut framework which parameter receives the data with the Body annotation.
The following example implements a simple echo server that echoes the body sent in the request:
Using the @Body annotation
Note that reading the request body is done in a non-blocking manner in that the request contents are read as the data becomes available and accumulated into the String passed to the method.
Tip
The micronaut.server.maxRequestSize setting in your configuration file (e.g. application.yml) limits the size of the data (the default maximum request size is 10MB) read/buffered by the server. @Size is not a replacement for this setting.
Regardless of the limit, for a large amount of data accumulating the data into a String in-memory may lead to memory strain on the server. A better approach is to include a Reactive library in your project (such as Reactor, RxJava,or Akka) that supports the Reactive streams implementation and stream the data it becomes available:
Using Reactive Streams to Read the request body
Important
Body arguments of types that do not require conversion cause the Micronaut framework to skip decoding of the request!
16.2 Reactive Responses
The previous section introduced the notion of Reactive programming using Project Reactor and Micronaut.
To use Project Reactor's Flux or Mono you need to add the Micronaut Reactor dependency to your project to include the necessary converters.
Note
To use RxJava's Flowable, Single or Maybe you need to add the Micronaut RxJava dependency to your project to include the necessary converters.
The argument designated as the body of the request using the Body annotation can also be a reactive type or a CompletableFuture.
When returning a reactive type, The Micronaut framework subscribes to the returned reactive type on the same thread as the request (a Netty Event Loop thread). It is therefore important that if you perform any blocking operations, you offload those operations to an appropriately configured thread pool, for example using the Project Reactor or RxJavasubscribeOn(..) facility or @ExecuteOn.
Tip
See the section on Configuring Thread Pools for information on the thread pools that the Micronaut framework sets up and how to configure them.
To summarize, the following table illustrates some common response types and their handling:
When returning a Reactive type, its type affects the returned response. For example, when returning a Flux, the Micronaut framework cannot know the size of the response, so Transfer-Encoding type of Chunked is used. Whilst for types that emit a single result such as Mono the Content-Length header is populated.
17 Reading the Body Asynchronously
A method argument of type AsyncRequestBody gives the body of the request without reading it. The method decides how to read it: decoded to a type, as text or bytes, element by element, written to a file, or taken as is. Each read returns a CompletionStage, so the method can return a stage and never block.
body decodes the same way as a Body argument: with the same message body readers and the same errors. A malformed JSON body is answered with 400 Bad Request, and a body larger than micronaut.server.max-request-buffer-size is answered with 413 Request Entity Too Large. The stage returned by the method fails with the exception, which the error handlers answer like they answer it for a Body argument.
Reading the Body
The other ways to read the body are:
text() reads the body as a string, in the charset of the request, up to micronaut.server.max-request-buffer-size bytes. text(maximumBytes) sets a lower limit, and text(maximumBytes, charset) reads the body in the given charset instead.
bytes(maximumBytes) reads the body into a byte[], up to maximumBytes and never more than micronaut.server.max-request-buffer-size bytes.
transferTo(path) writes the body to a new file, without holding it in memory, so micronaut.server.max-request-buffer-size does not limit it.
takeBody() hands the body over as a CloseableByteBody, unread. The method must then consume or close it, e.g. by sending it on with an HTTP client.
discardBody() discards the body without reading it.
body, text and bytes hold the whole body in memory, so micronaut.server.max-request-buffer-size always limits them, however the body is chunked and whenever it arrives. The maximumBytes of text and bytes lowers that limit and never raises it: the body is read up to the smaller of the two, and a larger body is answered with 413 Request Entity Too Large. To read a body larger than the buffer limit, read it with elements, which limits each element, write it to a file with transferTo, or take it with takeBody().
Reading the Body as Text
hasBody() and expectedBodySize() tell whether the request has a body and how large the client said it is, without reading it.
Warning
The body can be read once. The first read owns the body, and a second read, even with another method, throws an IllegalStateException. A body that is not read is discarded when the request ends. To read the body more than once, read a copy of it, see Copies of the Body.
What a read left open is released when the method completes, before the response is written or the filter chain continues: a read the method did not wait for, e.g. elements or transferTo whose stage it did not return, is aborted and its buffers and files are released, so the work on the body must be part of the stage the method returns. A controller method that answers with a streamed response, e.g. a Publisher of many items, keeps the body until the stream ends: when it completes, fails or is cancelled, e.g. because the client disconnected. The stream can be made of the reads of the body, e.g. one item for each element read with elements.
body and elements decode a whole value in memory, so they refuse the types that would read it in some other way: a Publisher, a CompletableFuture or another asynchronous type, and InputStream throw an IllegalArgumentException. To stream the body, read it element by element, or take it with takeBody().
Reading the Elements of a JSON Array or Stream
elements decodes a JSON array (application/json) or a JSON stream (application/x-json-stream, one value per line) one element at a time:
Reading the Elements
Only what the method asks for is read: the next element is received once the consumer is done with the previous one, so a slow consumer slows down the client instead of filling the memory of the server. next() reads a single element instead, and completes with an empty Optional at the end of the body. The elements that are not read are discarded.
The limit micronaut.server.max-request-buffer-size applies to each element, not to the whole body: an element larger than the limit fails with 413, while the body as a whole is only limited by micronaut.server.max-request-size. This holds even for a limit smaller than what the server receives before the method reads the elements, e.g. with the request headers.
A request of another media type fails the first read of the elements with 415 Unsupported Media Type.
Important
The readers that decode JSON one element at a time are part of micronaut-http-netty, which the Netty server includes. A server that is not the Netty server, e.g. a servlet container, needs micronaut-http-netty on its runtime classpath to read elements. Without it, the first read fails with an UnsupportedOperationException.
Tip
The stages complete on a thread chosen by the server, usually an event loop thread, so a consumer must not block. Return the stage of an asynchronous call instead, e.g. of a reactive database driver.
Copies of the Body
copy() returns another AsyncRequestBody over a copy of the bytes of the body. Reading the copy does not consume the body: the route, or another reader, can still read it in any way. The copy has its own single read, and so does the body it was copied from.
The bytes the copy reads are kept for the body until it is read, so a copy is held to micronaut.server.max-request-buffer-size as a whole, like a Body argument of a filter, and a larger body is answered with 413 Request Entity Too Large. This holds for the streaming reads of a copy too, elements, parts and transferTo: a copy is for looking at the body, not for streaming a large upload. form() of a copy is the one form of the request, which it reads from a split of the bytes, so the body stays readable in any way and the form is held to the limit as a whole, and parts() of a copy decodes the form a second time, from a split of the bytes.
In Filters
The request filter methods of a ServerFilter can declare an AsyncRequestBody argument too, and read the body before the route runs. The filter method returns the stage of the read, and the request continues once it completes.
The arguments of a filter look at the body, the reads of an AsyncRequestBody consume it, and the reads of a copy look at it:
The one form of the request, shared by the filters and the route
Reads the form in any way
body, text, bytes, elements, transferTo, form, parts, takeBody or discardBody of an AsyncRequestBody, the parts of a FormParts argument, or a FormPart argument
The body itself: what the filter does not read is discarded
Fails when it reads the body, with a Body, FormData, FileUpload or Part argument or an AsyncRequestBody, with 500 Internal Server Error. A route that does not read the body answers.
The same reads of copy()
A copy of the bytes, or the shared form for copy().form()
Reads the body in any way
The reads of an AsyncRequestBody in a filter are for a filter that consumes the body itself, e.g. to forward it with takeBody(), or to reject the request. When the route reads the body after them, the IllegalStateException of the failure names the read of the filter, e.g. The body of the request was already read with text() of an AsyncRequestBody, e.g. by a filter, which consumes it: read it with copy() in the filter to leave it for the route, or bind it with @Body.
To look at the body and leave it for the route, the filter reads a copy:
Reading a Copy of the Body in a Filter
The Route Reads the Body
A filter that reads the body or a copy of it buffers the body before the route runs, and the body is then held to micronaut.server.max-request-buffer-size as a whole. For a form, a FormData argument or copy().form() is the shared form, which the form arguments of the route do not decode a second time. After a FormData argument of a filter the route reads the form, not the bytes of the body. After copy().form() the route can still read the body in any way, e.g. with text() or a Body argument, since the copy keeps the bytes for it. copy().parts() decodes the form a second time, so it is only for a filter that must stream through the parts and still leave the body for the route. See Form Data and File Uploads.
form(FormOptions.defaults().withRawValues()) reads the fields of an application/x-www-form-urlencoded form with their values as they appear in the body, not percent-decoded: a%2Fb, a + that is not a space, and the percent-encoded bytes of a non-ASCII value, e.g. for a framework that must not decode a field. The names are decoded, and the form has the limits and the validation of a decoded form. It is not the shared form of the request: the form arguments do not share it, and it does not reuse a form they read, as it reads the bytes of the body, which it then needs: after another read consumed them, or when a filter replaced the body with an object, it fails. Of a copy it reads a split of the bytes and leaves the body for the route. A multipart/form-data form has no percent-encoding, so it is read like form(). The raw values of the path variables are read in a similar way, with raw() of PathVariables.
A filter that continues with a mutable request whose body it cleared, with request.body(null), leaves no body: hasBody() is false and text() is empty. A body the filter set to an object is read with body, which converts it like a Body argument.
18 JSON Binding
The most common data interchange format nowadays is JSON.
By default, the Controller annotation specifies that the controllers in Micronaut framework consume and produce JSON by default.
Since Micronaut Framework 4.0, users must choose how they want to serialize (Jackson Databind or Micronaut Serialization). Both approaches allow the usage of Jackson Annotations.
With either approach, the Micronaut framework reads incoming JSON in a non-blocking manner.
Kotlin Data Classes Jackson Databind Serialization
Warning
If you use Kotlin Data Classes and Jackson Databind. Your data classes will be accessed via reflection for serialization. When you use native image, you need to annotate those data classes with @ReflectiveAccess. Learn more about Micronaut GraalVM integration.
@ReflectiveAccess@Introspecteddata class Greeting(@JsonProperty("message") val message: String)
If you are intentionally writing implementation-specific code, you can still use the corresponding implementation API such as Jackson’s ObjectMapper or Micronaut Serialization’s ObjectMapper.
You can inject a bean of type JsonMapper or manually instantiate one via JsonMapper.createDefault().
18.4 Binding using Reactive Frameworks
From a developer perspective however, you can generally just work with Plain Old Java Objects (POJOs) and can optionally use a Reactive framework such as RxJava or Project Reactor. The following is an example of a controller that reads and saves an incoming POJO in a non-blocking way from JSON:
Using Reactive Streams to Read the JSON
Using cURL from the command line, you can POST JSON to the /people URI:
Using cURL to Post JSON
$ curl -X POST localhost:8080/people -d '{"firstName":"Fred","lastName":"Flintstone","age":45}'
18.5 Binding Using CompletableFuture
The same method as the previous example can also be written with the CompletableFuture API instead:
Using CompletableFuture to Read the JSON
@Controller("/people")public class PersonController { Map<String, Person> inMemoryDatastore = new ConcurrentHashMap<>(); @Post("/saveFuture") public CompletableFuture<HttpResponse<Person>> save(@Body CompletableFuture<Person> person) { return person.thenApply(p -> { inMemoryDatastore.put(p.getFirstName(), p); return HttpResponse.created(p); } ); }}
@Controller("/people")class PersonController { internal var inMemoryDatastore: MutableMap<String, Person> = ConcurrentHashMap() @Post fun save(@Body person: Person): HttpResponse<Person> { inMemoryDatastore[person.firstName] = person return HttpResponse.created(person) }}
@Controller("/people")class PersonController { Map<String, Person> inMemoryDatastore = new ConcurrentHashMap<>() @Post HttpResponse<Person> save(@Body Person person) { inMemoryDatastore.put(person.getFirstName(), person) HttpResponse.created(person) }}
The Micronaut framework only executes your method once the data has been read in a non-blocking manner.
Tip
You can customize the output in various ways, such as using Jackson annotations.
18.7 Jackson Configuration
If you use micronaut-jackson-databind, the Jackson ObjectMapper can be configured through configuration with the JacksonConfiguration class. This section is specific to the Jackson implementation; for application code that should remain portable across Micronaut JSON implementations, prefer depending on JsonMapper.
All Jackson configuration keys start with jackson.
If you use micronaut-jackson-databind, all Jackson’s features can be configured with their name as the key and a boolean to indicate enabled or disabled. Please check the configuration reference for the property names.
If you use micronaut-jackson-databind, there may be situations where you wish to customise the JsonFactory used by the ObjectMapper beyond the configuration of features (for example to allow custom character escaping).
This can be achieved by providing your own JsonFactory bean, or by providing a BeanCreatedEventListener<JsonFactory> which configures the default bean on startup.
18.7.3 Support for `@JsonView`
If you use micronaut-jackson-databind, you can use the @JsonView annotation on controller methods if you set jackson.json-view.enabled to true in your configuration file (e.g application.yml).
Jackson’s @JsonView annotation lets you control which properties are exposed on a per-response basis. See Jackson JSON Views for more information.
18.7.4 Beans
If you use micronaut-jackson-databind, in addition to configuration, beans can be registered to customize Jackson. All beans that extend any of the following classes are registered with the object mapper:
Any modules registered via the service loader are also added to the default object mapper.
18.7.6 Number Precision
During JSON parsing, the framework may convert any incoming data to an intermediate object model. By default, this model uses BigInteger, long and double for numeric values. This means some information that could be represented by BigDecimal may be lost. For example, numbers with many decimal places that cannot be represented by double may be truncated, even if the target type for deserialization uses BigDecimal. Metadata on the number of trailing zeroes (BigDecimal.precision()), e.g. the difference between 0.12 and 0.120, is also discarded.
If you need full accuracy for number types, use the following configuration:
By default, a Micronaut Controller responds with content-type application/json. However, you can respond with content type text/plain by annotating the controller method with the @Produces annotation.
HTTP Response with text/plain Content-Type
Note
Micronaut Framework 4.x text/plain responses are more restrictive about allowed types than Micronaut Framework 3.x. To return plain text responses for answers other than java.lang.String, manually call the object toString() method. Alternatively, set the micronaut.http.legacy-text-conversion configuration option to true to restore the old – but not recommended – Micronaut Framework 3.x behavior.
20 Data Validation
It is easy to validate incoming data with Micronaut controllers using Validation Advice.
The Micronaut framework provides native support for the jakarta.validation annotations with the micronaut-validation dependency:
We can validate parameters using jakarta.validation annotations and the Validated annotation at the class level.
Example
If a validation error occurs a jakarta.validation.ConstraintViolationException is thrown. By default, the integrated io.micronaut.validation.exception.ConstraintExceptionHandler handles the exception, leading to a behaviour as shown in the following test:
Annotate your controller with Validated, specifying the validation groups that will be active or letting it default to Default. Also annotate the binding POJO with @Valid.
Example
Validation of POJOs using the default validation group is shown in the following test:
To serve static resources with the settings of a route, e.g. filters, conditions or a Cache-Control header, declare them as a functional route, see Static Resources of the functional routes.
22 Error Handling
Sometimes with distributed applications, bad things happen. Having a good way to handle errors is important.
22.1 Status Handlers
The @Error annotation supports defining either an exception class or an HTTP status. Methods annotated with @Error must be defined within a class annotated with @Controller. The annotation also supports the notion of global and local, local being the default.
Local error handlers only respond to exceptions thrown as a result of the route being matched to another method in the same controller. Global error handlers can be invoked as a result of any thrown exception. A local error handler is always searched for first when resolving which handler to execute.
Tip
When defining an error handler for an exception, you can specify the exception instance as an argument to the method and omit the exception property of the annotation.
For example, the following method handles JSON parse exceptions from Jackson for the scope of the declaring controller:
Local exception handler
Local status handler
Similar to other controller methods, error handlers can use request binding annotations on parameters e.g.
to access header values. However, binding the request body comes with additional restrictions depending on the HTTP
server implementation used. If the body has already been bound to a parameter of the original controller method, it may
not be possible to bind the body to a different type, as the original bytes may already have been discarded.
22.3 Global Error Handling
Global error handler
Global status handler
Important
A few things to note about the @Error annotation. You cannot declare identical global @Error annotations. Identical non-global @Error annotations cannot be declared in the same controller. If an @Error annotation with the same parameter exists as global and another as local, the local one takes precedence.
22.4 ExceptionHandler
Alternatively, you can implement an ExceptionHandler, a generic hook for handling exceptions that occur during execution of an HTTP request.
Important
An @Error annotation capturing an exception has precedence over an implementation of ExceptionHandler capturing the same exception.
22.4.1 Built-In Exception Handlers
The Micronaut framework ships with several built-in handlers:
HTML error responses are provided via a bean of type HtmlErrorResponseBodyProvider.
The default implementation outputs HTML which can be localized with codes such as:
<status>.error.bold, <status>.error.title, <status>.error. For example, you could localize the default 404 error page into Spanish:
404.error.bold=La página que buscabas no existe404.error.title=No encontrado404.error=Es posible que haya escrito mal la dirección o que la página se haya movido.
For unhandled exceptions, the default error response does not include the exception message.
This avoids exposing implementation details to HTTP clients while the exception remains available in server logs.
To include these messages for compatibility or debugging, set micronaut.server.error-response-include-message to always.
The supported values are never (the default), always, and on-param. The on-param value includes the message when the request has a message query parameter whose value is not false.
Enabling this option may expose sensitive implementation details and is not recommended for production environments.
If customization of the response other than items related to the errors is desired, the exception handler that is handling the exception needs to be overridden.
23 API Versioning
Since 1.1.x, the Micronaut framework supports API versioning via a dedicated @Version annotation.
The following example demonstrates how to version an API:
Versioning an API
Then enable versioning by setting micronaut.router.versioning.enabled to true in your configuration file (e.g application.yml):
By default, the Micronaut framework has two strategies for resolving the version based on an HTTP header named X-API-VERSION or a request parameter named api-version, however this is configurable. A full configuration example can be seen below:
parameter.enabled enables or disables parameter-based versioning
parameter.names specifies the parameter names as a comma-separated list
header.enabled enables or disables header-based versioning
header.names specifies the header names as a list
If this is not enough you can also implement the RequestVersionResolver interface which receives the HttpRequest and can implement any strategy you choose.
Default Version
It is possible to supply a default version through configuration.
The default key refers to the default configuration. You can specify client-specific configuration by using the value passed to @Client (typically the service ID). For example:
The above uses a key named greeting-service which can be used to configure a client annotated with @Client('greeting-service').
24 Handling Form Data
To make data binding model customizations consistent between form data and JSON, the Micronaut framework uses Jackson to implement binding data from form submissions.
The advantage of this approach is that the same Jackson annotations used for customizing JSON binding can be used for form submissions.
Important
Form URL encoded content type and Jackson annotations are not supported by the Micronaut HTTP Client.
In practice this means that to bind regular form data, the only change required to the previous JSON binding code is updating the MediaType consumed:
@Controller("/people")class PersonController { val inMemoryDatastore: MutableMap<String, Person> = ConcurrentHashMap() @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Post fun save(@Body person: Person): HttpResponse<Person> { inMemoryDatastore[person.firstName!!] = person return HttpResponse.created(person) }}
@Controller("/people")class PersonController { Map<String, Person> inMemoryDatastore = new ConcurrentHashMap<>() @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Post HttpResponse<Person> save(@Body Person person) { inMemoryDatastore.put(person.getFirstName(), person) HttpResponse.created(person); }}
Tip
To avoid denial-of-service attacks, collection types and arrays created during binding are limited by the setting jackson.arraySizeThreshold in your configuration file (e.g application.yml). The nesting depth of a form key is limited to 100 levels, and the total array padding of a request is limited to the threshold plus the length of its keys.
Alternatively, instead of using a POJO you can bind form data directly to method parameters (which works with JSON too!):
Binding Form Data to Parameters
@Controller("/people")class PersonController { Map<String, Person> inMemoryDatastore = new ConcurrentHashMap<>(); @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Post("/saveWithArgs") HttpResponse<Person> save(String firstName, String lastName, @Nullable Integer age) { Person p = new Person(firstName, lastName); if (age != null) { p.setAge(age); } inMemoryDatastore.put(p.getFirstName(), p); return HttpResponse.created(p); }}
imports
from micronaut.http import HttpResponse, MediaTypefrom micronaut.http.annotation import Body, Consumes, Controller, Postfrom .Person import Person
@Requires(property="spec.name", value="PersonControllerFormTest")@Controller("/people")class PersonController: inMemoryDatastore: dict[str, Person] = {} @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Post("/saveWithArgs") def saveWithArgs(self, firstName: str, lastName: str, age: int | None = None) -> HttpResponse: person = Person(firstName, lastName) if age is not None: person.setAge(age) self.inMemoryDatastore[person.getFirstName()] = person return HttpResponse.created(person)
@Controller("/people")class PersonController { val inMemoryDatastore: MutableMap<String, Person> = ConcurrentHashMap() @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Post("/saveWithArgs") fun save(firstName: String, lastName: String, age: Int?): HttpResponse<Person> { val p = Person(firstName, lastName) if (age != null) { p.age = age } inMemoryDatastore[p.firstName!!] = p return HttpResponse.created(p) }}
@Controller("/people")class PersonController { Map<String, Person> inMemoryDatastore = new ConcurrentHashMap<>() @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Post("/saveWithArgs") HttpResponse<Person> save(String firstName, String lastName, @Nullable Integer age) { Person p = new Person() p.firstName = firstName p.lastName = lastName if (age != null) { p.setAge(age) } inMemoryDatastore.put(p.firstName, p) return HttpResponse.created(p); }}
As you can see from the example above, this approach lets you use features such as support for @Nullable or Optional types and restrict the parameters to be bound. When using POJOs you must be careful to use Jackson annotations to exclude properties that should not be bound.
Tip
To read the fields of a form without binding them to a POJO, e.g. when the names are not known in advance, declare a FormData argument, see Form Data and File Uploads.
25 Forms
The Micronaut HTTP server includes support for forms, both using multipart/form-data and using application/x-www-form-urlencoded format.
25.1 Form Data Binding
For a typical web application, the low-level form API is unnecessary. Instead, you will bind individual form fields to controller parameters of different types.
When you bind at least one form field to a controller parameter, the framework will take over the FormCapableHttpRequest, making the body only available through controller argument binding.
There are various buffered and streaming types you can bind form fields to.
The simplest binding is a String. The framework will wait for the form field to arrive, and then call your controller with the full value of the form field.
@Controller("/form")public class FormController { @Consumes(MediaType.MULTIPART_FORM_DATA) @Post("/string") public String setUserName(int userId, String userName) { if (!userName.matches("[a-z]+")) { throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Invalid username"); } return "New user name for user ID " + userId + ": " + userName; }}
@Controller("/form")class FormController { @Consumes(MediaType.MULTIPART_FORM_DATA) @Post("/string") fun setUserName(userId: Int, userName: String): String { if (!userName.matches(Regex("[a-z]+"))) { throw HttpStatusException(HttpStatus.BAD_REQUEST, "Invalid username") } return "New user name for user ID $userId: $userName" }}
@Controller("/form")class FormController { @Consumes(MediaType.MULTIPART_FORM_DATA) @Post("/string") String setUserName(int userId, String userName) { if (!(userName ==~ /[a-z]+/)) { throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Invalid username") } "New user name for user ID $userId: $userName" }}
It is also possible to bind to other simple types, such as int or byte[].
Note that if there are multiple form fields of the same name, only the first one is bound. Any later fields are discarded.
CompletedFileUpload
A CompletedFileUpload is similar to a byte[] parameter, except that the upload can be buffered to disk instead of memory. This allows you to receive even large files without consuming a lot of memory. This behavior needs to be configured, though.
If you don’t configure this value, the file will be kept in memory. The controller is called once the entire file has been uploaded.
Note
The uploaded file is automatically deleted when the controller terminates, so if you want to keep it around, you need to move or copy it elsewhere beforehand. You can also call CompletedFileUpload.moveResource() to get a new CompletedFileUpload instance that sticks around after the controller lifetime.
CompletedFileUpload controller example
StreamingFileUpload
A StreamingFileUploadlazily streams a form field. Your controller can be called before the full content is received. Similar to RawFormField, you must consume the input data so that the upload does not stall.
The advantage of StreamingFileUpload over CompletedFileUpload is that you do not need any disk space, and that you can start streaming the data immediately to e.g. forward it to a backend service. If the backend is slow to receive the uploaded data, this backpressure will also apply to the browser upload, and that upload will slow down – there is no (significant) buffering in the Micronaut service, unlike with CompletedFileUpload.
StreamingFileUpload controller example
Note that using StreamingFileUpload in combination with other form fields can be tricky. To run your controller, all "normal" form field parameters must have been received, but a StreamingFileUpload will pause the request until you consume it in your controller. If the normal parameters actually come after the file upload in the request, this creates a "deadlock". The Micronaut HTTP server will produce an error in this scenario. One possible solution is to replace the "normal" form parameters with publisher-based parameters so that the controller can be executed before those fields are received.
Publishers
Publisher parameters let you handle various special scenarios, such as asynchronous binding or handling multiple form fields of the same name.
… of completed parts
A publisher of CompletedPart, CompletedAttribute or CompletedFileUpload contains the buffered content of the form field. The publisher can produce multiple items if there are multiple values for the same field name.
Note that there is no backpressure on this publisher. To avoid deadlocks with other parameters, the fields will be read in their entirety immediately, even if you haven’t subscribed to the publisher yet. Configured form field count and size limits still apply.
Publisher<CompletedFileUpload> controller example
… of StreamingFileUpload
Similar to the StreamingFileUpload parameter described above, except that the controller can be called before the StreamingFileUpload is received, and that the publisher can contain multiple uploads of the same name.
Once again, the StreamingFileUpload instances exert backpressure on the request, so you must consume them in their entirety before other fields can be processed.
Publisher<StreamingFileUpload> controller example
… of Publisher
A Publisher<Publisher<…>>> behaves similar to Publisher<StreamingFileUpload> as described above, allowing you to stream individual fields.
A PartData object represents a piece of a form field, containing the field metadata and a piece of the field value buffered in memory. For large fields and file uploads, a single field may produce multiple PartData objects. If there are multiple fields of the same name, they will also produce multiple PartData instances.
This publisher exerts backpressure as described for StreamingFileUpload.
A publisher of any other type will be created by taking a publisher of completed parts as described above, and converting the parts to the target type. For file uploads where a media type is given, this may include conversion from e.g. JSON.
25.2 Detailed Form API
The lowest-level form API in the Micronaut HTTP server is the FormCapableHttpRequest. It provides a reactive Publisher of the form fields. This publisher can be subscribed to at most once. Each field is represented by a RawFormField containing the field metadata and field body as a ByteBody.
Field Metadata
The most important information in the field metadata is the field name. This corresponds to the name attribute on a form input element.
File uploads contain additional metadata: The file name, and optionally, the file content type.
Note that there is no restriction on this metadata. Multiple fields may have the same name (browsers will do this when uploading multiple files), and any of the metadata fields may be missing entirely.
Field Body
The field body is represented by a ByteBody, which is an asynchronous, streaming API. Large form fields may be processed lazily: As long as you do not consume the data from the ByteBody, the form data will not be read from the client, and e.g. an upload may stall. You will also not see any other fields on the main publisher, since they can only be received once all data for the current form field has arrived.
For short form fields, the ByteBody may be fully buffered internally, but you should not rely on this.
26 Writing Response Data
Reactively Writing Response Data
Micronaut’s HTTP server supports writing chunks of response data by returning a Publisher that emits objects that can be encoded to the HTTP response.
The following table summarizes example return type signatures and the behaviour the server exhibits to handle them:
Return Type
Description
Publisher<String>
A Publisher that emits each chunk of content as a String
Flux<byte[]>
A Flux that emits each chunk of content as a byte[] without blocking
Flux<ByteBuf>
A Reactor Flux that emits each chunk as a Netty ByteBuf
Flux<Book>
When emitting a POJO, each emitted object is encoded as JSON by default without blocking
Flowable<byte[]>
A Flux that emits each chunk of content as a byte[] without blocking
Flowable<ByteBuf>
A Reactor Flux that emits each chunk as a Netty ByteBuf
Flowable<Book>
When emitting a POJO, each emitted object is encoded as JSON by default without blocking
When returning a reactive type, the server uses a Transfer-Encoding of chunked and keeps writing data until the PublisheronComplete method is called.
The server requests a single item from the Publisher, writes it, and requests the next, controlling back pressure.
Note
It is up to the implementation of the Publisher to schedule any blocking I/O work that may be done as a result of subscribing to the publisher.
Note
To use Project Reactor's Flux or Mono you need to add the Micronaut Reactor dependency to your project to include the necessary converters.
Note
To use RxJava's Flowable, Single or Maybe you need to add the Micronaut RxJava dependency to your project to include the necessary converters.
Performing Blocking I/O
In some cases you may wish to integrate a library that does not support non-blocking I/O.
Writable
In this case you can return a Writable object from any controller method. The Writable interface has various signatures that allow writing to traditional blocking streams like Writer or OutputStream.
When returning a Writable, the blocking I/O operation is shifted to the I/O thread pool so the Netty event loop is not blocked.
Tip
See the section on configuring Server Thread Pools for details on how to configure the I/O thread pool to meet your application requirements.
The following example demonstrates how to use this API with Groovy’s SimpleTemplateEngine to write a server side template:
Performing Blocking I/O With Writable
InputStream
Another option is to return an input stream. This is useful for many scenarios that interact with other APIs that expose a stream.
The input stream is returned and its contents will be the response body
Note
The reading of the stream will be offloaded to the IO thread pool if the controller method is executed on the event loop.
404 Responses
Often, you want to respond 404 (Not Found) when you don’t find an item in your persistence layer or in similar scenarios.
See the following example:
Note
Responding with an empty Publisher or Flux is a streaming response, which results in an empty array being returned if the content type is JSON. Annotate the method with SingleResult to disable streaming.
To disable 404 on a null body or an empty publisher set micronaut.server.not-found-on-missing-body to false.
27 File Uploads
Handling of file uploads has special treatment in Micronaut. Support is provided for streaming of uploads in a non-blocking manner through streaming uploads or completed uploads.
To receive data from a multipart request, set the consumes argument of the method annotation to MULTIPART_FORM_DATA. For example:
To read the files and fields of a form with CompletionStage based types instead of publishers, see Form Data and File Uploads.
Route Arguments
Method argument types determine how files are received. Data can be received a chunk at a time or when an upload is completed.
Tip
If the route argument name cannot or should not match the name of the part in the request, add the Part annotation to the argument and specify the expected name in the request.
Chunk Data Types
PartData represents a chunk of data received in a multipart request. PartData interface methods convert the data to a byte[], InputStream, or a ByteBuffer.
Note
Data can only be retrieved from a PartData once. The underlying buffer is released, causing further attempts to fail.
Route arguments of type Publisher are treated as intended to receive a single file, and each chunk of the received file will be sent downstream. If the generic type is other than PartData, conversion will be attempted using Micronaut’s conversion service. Conversions to String and byte[] are supported by default.
If you need knowledge about the metadata of an uploaded file, the StreamingFileUpload class is a Publisher that also has file information such as the content type and file name.
Streaming file upload
It is also possible to pass an output stream with the transferTo method.
Note
The reading of the file or stream will be offloaded to the IO thread pool to prevent the possibility of blocking the event loop.
Streaming file upload
Whole Data Types
Route arguments that are not publishers cause route execution to be delayed until the upload has finished. The received data will attempt to be converted to the requested type. Conversions to a String or byte[] are supported by default. In addition, the file can be converted to a POJO if a media type codec is registered that supports the media type of the file. A media type codec is included by default that allows conversion of JSON files to POJOs.
If you need knowledge about the metadata of an uploaded file, the CompletedFileUpload class has methods to retrieve the data of the file, and also file information such as the content type and file name.
File upload with metadata
Important
If a file will not be read, the discard method on the file object must be called to prevent memory leaks.
Multiple Uploads
Different Names
If a multipart request has multiple uploads that have different part names, create an argument to your route that receives each part. For example:
HttpResponse upload(String title, String name)
A route method signature like the above expects two different parts, one named "title" and the other "name".
Same Name
To receive multiple parts with the same part name, the argument must be a Publisher. When used in one of the following ways, the publisher emits one item per part found with the specified name. The publisher must accept one of the following types:
When request part names aren’t known ahead of time, or to read the entire body, a special type can be used to indicate the entire body is desired.
If a route has an argument of type MultipartBody (not to be confused with the class for the client) annotated with @Body, each part of the request will be emitted through the argument. A MultipartBody is a publisher of CompletedPart instances.
The FormData, FileUpload, FormPart and FormParts types read a submitted form, application/x-www-form-urlencoded or multipart/form-data, without a Publisher. Every operation that reads content returns a CompletionStage, so a method can return a stage and never block.
Note
These types are experimental. Their API may change in a future release.
The Whole Form
A method argument of type FormData receives the whole form, read before the method runs:
Reading the Whole Form
Text fields convert with accessors like the ones of path variables: getString, getInt and the other types for a required field, the same with a default value for an optional one, findString, findInt and the other find methods, and get(name, type) for any other type. getValues returns every value of a field, e.g. of checkboxes, and get with an Argument of a collection, e.g. Argument.listOf(Integer.class), converts every value. getFile, findFile and getFiles return the uploaded files of a field.
A required field that is missing, or a value that does not convert, is answered with 400 Bad Request.
The uploaded files are stored in memory or on disk, depending on the micronaut.server.multipart configuration, see File Uploads. A file larger than micronaut.server.multipart.max-file-size is answered with 413 Request Entity Too Large.
Binding Files and Fields
A method can also declare the files and text fields it needs as arguments, bound by name or with the Part annotation:
Binding Files and Fields
The arguments share one form: a FormData argument, the FileUpload arguments and the text field arguments of a method are all read from the same form, and form.getFile("avatar") returns the same FileUpload as an avatar argument. A required file or field that is missing is answered with 400 Bad Request.
Consuming an Upload
The content of a FileUpload can be consumed once, with one of:
bytes(maximumBytes), which reads the content into memory
text(maximumBytes), which reads the content into memory as text, in the charset of the part’s Content-Type, else the charset of the request, else the default charset of the server. text(maximumBytes, charset) reads it in the given charset instead.
transferTo(path), which writes the content to a new file. The file must not exist, and its directory must exist. Once written, the file belongs to the application.
transferTo(outputStream), which writes the content to a stream
takeBody(), which hands the content over as a CloseableByteBody, which the caller must then consume or close
None of them blocks: the reads return a CompletionStage, whether the content is in memory, on disk or still arriving, and content stored on disk is read on the I/O executor. A second consumption throws an IllegalStateException. The name, file name, content type and size stay readable after the content was consumed.
Warning
fileName() is the name the client sent. Do not use it as a path without sanitizing it.
An upload belongs to the request. When the request ends, what the application did not consume is released, including the fields that were still waiting to be read when the reading stopped, and an operation that is still running is aborted. The work on an upload must therefore be part of the stage the method returns, as in the examples above.
Warning
Do not close an upload in a try-with-resources block around an asynchronous operation: the block closes it as soon as the operation starts, which aborts the operation.
close() releases an upload early, and FormData.close() releases all the files of a form. The text fields stay readable.
Streaming a Part
A FormPart argument receives a part of the form as it arrives, without storing it first:
Streaming a Part
The method runs once the part starts arriving, so the text fields it needs must be sent before the part. A field sent after the part cannot be bound, and the request is answered with 400 Bad Request. A FormPart is a text field or a file: isFile() tells which, text() reads a text field, in the charset of the part like a file’s text, and file() gives the part as a FileUpload. An Optional<FormPart> argument is empty when the form has no such part.
Like the reads of an AsyncRequestBody, an operation on the part that is still running when the method completes, e.g. a transferTo whose stage the method did not return, is aborted and its buffers and files are released before the response is written, or, for a streamed response, when its stream ends. The work on the part must therefore be part of the stage the method returns. Content the method did not read is discarded when the request ends.
Reading the Parts One by One
A FormParts argument reads the whole form part by part, in the order the client sent them:
Reading the Parts
The consumer owns its part until the stage it returned completes. Then what it did not read is discarded, and the next part is read. part(name, consumer) reads forward to the part of a name, discarding the parts before it, and completes with false when the form has no such part. closeAsync() discards the rest of the form.
The parts are closed like the reads of an AsyncRequestBody: when the method completes, normally or exceptionally, before the response is written, or, for a method that answers with a streamed response, when its stream ends: completes, fails or is cancelled, e.g. because the client disconnected. An operation the method did not wait for is aborted then, the part it was consuming is released, and the rest of the form is discarded, so the work on the parts must be part of the stage the method returns, or of its stream.
A form is decoded as it arrives, so micronaut.server.max-request-buffer-size does not limit the form as a whole: it limits what is read into memory. This holds even for a limit smaller than what the server receives before the form is read, e.g. with the request headers. Only the form read from a copy of the body, copy().form() or copy().parts() of an AsyncRequestBody, is held to the limit as a whole, see Reading the Body Asynchronously.
The text fields of a FormData are held in memory, and are limited together: a form whose text fields are larger than micronaut.server.max-request-buffer-size is answered with 413 Request Entity Too Large. The text field and FileUpload arguments share that form, and its limit.
The files of a FormData are stored as the micronaut.server.multipart configuration says, and each is limited by micronaut.server.multipart.max-file-size.
The parts of FormParts and a FormPart are limited one by one, by the operation that reads them: text() reads up to micronaut.server.max-request-buffer-size bytes, text(maximumBytes) and bytes(maximumBytes) up to the given limit, and transferTo is not limited by the buffer size. A part larger than the limit of its read fails with 413 Request Entity Too Large.
The request as a whole is limited by micronaut.server.max-request-size, and the Netty server limits the number of fields and the bytes the decoder buffers with its micronaut.server.netty.form-max-fields and micronaut.server.netty.form-max-buffered-bytes settings.
Combining Arguments
A method can combine a FormData argument, FileUpload arguments and text field arguments, as they share one form. The other combinations read the form in ways that exclude each other, and the request is answered with 500 Internal Server Error with a message that says the arguments "cannot be combined":
The members of a RequestBean are bound like the arguments of the method: a FormData, a FileUpload, a text field with Part, a FormPart, FormParts, an AsyncRequestBody and PathVariables can be members of the bean. The method waits for a form that is still arriving before it is invoked with the bean, as it does for its arguments.
An AsyncRequestBody argument is different: its form() and parts() consume the body, like its other reads. So does a FormParts argument, once the filter reads a part, and its parts are closed when the filter method completes, like the reads of an AsyncRequestBody. A FormPart argument of a filter streams its part the same way: the fields before it are discarded, the filter runs once the part starts, and the rest of the form is discarded when the filter method completes. The route can then no longer read the form, and fails with 500 Internal Server Error when it does, while the files the filter did not read are released when the request ends. A filter that reads the form of an AsyncRequestBody and leaves it for the route reads it from a copy: copy().form() is the same shared form as a FormData argument, read from a split of the body, and copy().parts() streams through the parts of a split of the body. The body keeps the bytes for the route, which can read them in any way, so the form is then held to micronaut.server.max-request-buffer-size as a whole. See Reading the Body Asynchronously.
A filter that continues with a mutable request whose body it cleared, with request.body(null), leaves an empty form for the route. A filter that sets a FormData as the body replaces the form.
Note
With micronaut-http-validation on the annotation processor path, these parameters and Part are compilation errors on a response filter method and on the methods of a ClientFilter. So is a Part parameter of a request filter method whose type is read with the arguments of the route, after the filters: CompletedFileUpload, StreamingFileUpload, CompletedPart, PartData or a Publisher. A filter reads a file with a FileUpload, a FormPart or a FormData argument instead.
29 File Transfers
The Micronaut framework supports sending files to the client in a couple of easy ways.
Sending File Objects
It is possible to return a File object from your controller method, and the data will be returned to the client. The Content-Type header of file responses is calculated based on the name of the file.
To control either the media type of the file being sent, or to set the file to be downloaded (i.e. using the Content-Disposition header), instead construct a SystemFile with the file to use. For example:
Sending a SystemFile
@Getpublic SystemFile download() { File file = ... return new SystemFile(file).attach("myfile.txt"); // or new SystemFile(file, MediaType.TEXT_HTML_TYPE)}
Sending an InputStream
For cases where a reference to a File object is not possible (for example resources in JAR files), the Micronaut framework supports transferring input streams. To return a stream of data from the controller method, construct a StreamedFile.
Tip
The constructor for StreamedFile also accepts a java.net.URL for your convenience.
Sending a StreamedFile
@Getpublic StreamedFile download() { InputStream inputStream = ... return new StreamedFile(inputStream, MediaType.TEXT_PLAIN_TYPE) // An attach(String filename) method is also available to set the Content-Disposition}
The server supports returning 304 (Not Modified) responses if the files being transferred have not changed, and the request contains the appropriate header. In addition, if the client accepts encoded responses, the Micronaut framework encodes the file if appropriate. Encoding happens if the file is text-based and larger than 1KB by default. The threshold at which data is encoded is configurable. See the server configuration reference for details.
Warning
Avoid using PipedInputStream / PipedOutputStream as the backing source for StreamedFile or other InputStream-based HTTP responses. PipedInputStream tracks the liveness of the reader and writer threads and can report a broken pipe when the response is consumed on short-lived threads. This is especially relevant when the blocking executor uses virtual threads. Prefer a stable stream source such as a file, URL, or a reactive Publisher response. If you must use piped streams, make sure both ends stay bound to long-lived platform threads for the lifetime of the transfer.
Sending Reactive Streams as File Downloads
Micronaut also supports returning reactive streams (e.g., Flux, Flowable,
or any Publisher) without buffering the entire response in memory. If you want to
force the client browser to download the streamed data (for example, CSV lines),
you can set the Content-Disposition: attachment header.
Wrapping your stream in HttpResponse<Flux<String>> does not cause
the entire CSV to be loaded into memory. Micronaut will still stream
the data as it is produced. Returning HttpResponse<T> simply allows you
to set any headers or custom status codes.
Cache Configuration
By default, file responses include caching headers. The following options determine how the Cache-Control header is built.
30 HTTP Filters
The Micronaut HTTP server supports applying filters to request/response processing in a similar (but reactive) way to Servlet filters in traditional Java applications.
You can use different styles of pattern for path matching by setting patternStyle. By default, AntPathMatcher is used for path matching. When using Ant, the mapping matches URLs using the following rules:
The other option is regular expression based matching. To use regular expressions, set patternStyle = FilterPatternStyle.REGEX. The pattern attribute is expected to contain a regular expression which will be expected to match the provided URLs exactly (using Matcher#matches).
Note
Using FilterPatternStyle.ANT is preferred as the pattern matching is more performant than using regular expressions. FilterPatternStyle.REGEX should be used when your pattern cannot be written properly using Ant.
30.2 Filter Methods
A filter method must be declared in a bean annotated with ServerFilter, or ClientFilter if it should instead intercept requests made by the HTTP client. Each filter method must also be annotated with RequestFilter, to run before the request is processed, or ResponseFilter, to run after the request has completed to process the response.
Note
Request filters can be executed before the route is matched if annotated with PreMatching.
A filter method can take various parameters, such as the HttpRequest and the HttpResponse (only for response filters). The return type can be void or null to continue execution as normal, or an updated HttpRequest (only for request filters) or HttpResponse. The different supported parameter and return types are described in the documentation of RequestFilter and ResponseFilter.
Request filters can bind the full request body using the Body annotation. This is useful e.g. to
verify a signed request body.
Important
Accessing the full body should be done with care. It forces the body to be fully buffered, even if the
controller supports streaming (e.g. @Body InputStream parameter), leading to potential negative performance impact.
Important
Body binding is supported by the netty-based HTTP server, but may not be supported by all other HTTP server
backends.
The request filter methods of a ServerFilter can also declare the parameters that read the body of the request in a controller method:
With micronaut-http-validation on the annotation processor path, the body parameters are compilation errors on response filter methods and on the methods of a ClientFilter, and PathVariables is a compilation error on PreMatching and client filter methods.
To write asynchronous filters, you can return a reactive publisher.
To put these concepts into practice lets look at an example.
Important
Filter methods execute in the event loop by default. If you need to perform blocking operations, you can annotate the filter with ExecuteOn.
30.2.1 Server Filter with Filter Methods
Suppose you wish to trace each request to the "Hello World" example using some external system. This system could be a database or a distributed tracing service, and may require I/O operations.
You should not block the underlying Netty event loop in your filter; instead the filter should proceed with execution once any I/O is complete.
As an example, consider this TraceService that performs an I/O operation:
A TraceService Example using Reactive Streams
The following code sample shows how to write a filter using filter methods:
An Example ServerFilter
The previous example demonstrates some key concepts such as executing blocking logic in a worker thread before proceeding with the request and modifying the outgoing response.
30.2.2 Proceeding with Filter Methods
If you need to write a filter, e.g., security-related, which needs to proceed with requests in some scenarios or stop the request execution
and return an HTTP Response directly in the filter; you can use, for example, a CompletableFuture as the filter method’s response type.
In principle, downstream filters and controllers can produce exceptions, and response filters should be prepared to handle them. For a response filter to be called when there is an exception, it must declare the exception type as a parameter.
Table 1. @Filter Response filter declaration
Declaration
Called when?
void responseFilter(HttpResponse<?> response)
Only called on non-exception response
void responseFilter(Throwable failure)
Only called on exception response
void responseFilter(IOException failure)
Only called on exception response, if the exception is an IOException
Always called. failure will be null if there was no error. If there was an error, response will be null.
Whether errors appear as exceptions depends on the context of the filter. For the Micronaut HTTP server, any exception is mapped to a non-exceptional HttpResponse with an error status code. This mapping happens before each filter, so a server filter will never actually see an exception. If you still want to access the original cause of the response, it is stored as the attribute EXCEPTION.
30.2.4 Continuations
Request filters can define a special FilterContinuation parameter to get more control of the downstream execution, and to be run further actions after it completes. For example, the above TraceFilter can be expressed using a single request filter:
Single request filter
Important
The call to FilterContinuation.proceed is blocking by default, so it should never be done on the event loop. Such filters should be run on a worker thread as described above. Alternatively, the continuation can also be declared to return a reactive type (Publisher<HttpResponse<?>>) to proceed in an asynchronous manner, similar to the old FilterChain API.
A continuation can also return CompletionStage<HttpResponse<?>> or CompletableFuture<HttpResponse<?>>. Its proceed() starts downstream work immediately and returns without blocking. The filter must return the response stage and may call proceed() only once. Stage callbacks run with the propagated downstream context and must not block an event loop.
Cancelling the continuation future, a future derived from it, or the filter chain sends a cancellation hint downstream. External exceptional completion, including orTimeout, of the original or derived continuation future also sends this hint. Propagation through derived futures is specific to filter continuations: ordinary dependent client stages do not abort their source operation. This hint does not release a response body already handed to a caller; its owner must close it.
30.2.5 Filter Order
Filters can be ordered by annotating the filter method or filter class with Order or jakarta.annotation.Priority, or by implementing Ordered in the filter class.
Request filters are executed in order from the highest precedence (the smallest integer value, as defined by Ordered.HIGHEST_PRECEDENCE) to the lowest precedence (the biggest integer value, as defined by Ordered.LOWEST_PRECEDENCE). Response filters are executed in reverse order.
Request filter A is executed first, because it has the higher precedence (-100), followed by request filter B with the lower precedence (100). Then the controller is executed. Response filter C is executed first, because it has the lower precedence (100), and finally response filter D with the higher precedence (-100) is executed last.
30.3 HttpServerFilter
Warning
We recommend Micronaut developers use Filter methods instead of HttpServerFilter introduced in Micronaut Framework 4.0 to implement filters.
For a server application, the HttpServerFilter interface doFilter method can be implemented.
The ServerFilterChain interface contains a resolved chain of filters where the final entry in the chain is the matched route. The ServerFilterChain.proceed(io.micronaut.http.HttpRequest) method resumes processing of the request.
The proceed(..) method returns a Reactive Streams Publisher that emits the response to be returned to the client. Implementors of filters can subscribe to the Publisher and mutate the emitted MutableHttpResponse to modify the response prior to returning the response to the client.
To put these concepts into practice lets look at an example.
Important
Filters execute in the event loop, so blocking operations must be offloaded to another thread pool.
30.3.1 HttpServerFilter Example
Suppose you wish to trace each request to the "Hello World" example using some external system. This system could be a database or a distributed tracing service, and may require I/O operations.
You should not block the underlying Netty event loop in your filter; instead the filter should proceed with execution once any I/O is complete.
As an example, consider this TraceService that uses Project Reactor to compose an I/O operation:
A TraceService Example using Reactive Streams
The following code sample shows how to implement the HttpServerFilter interface.
An Example HttpServerFilter
The previous example demonstrates some key concepts such as executing logic in a non-blocking manner before proceeding with the request and modifying the outgoing response.
Tip
The examples use Project Reactor, however you can use any reactive framework that supports the Reactive streams specifications
30.3.2 HttpServerFilter Error States
The publisher returned from chain.proceed should never emit an error. In the cases where an upstream filter emitted an error or the route itself threw an exception, the error response should be emitted instead of the exception. In some cases it may be desirable to know the cause of the error response and for this purpose an attribute exists on the response if it was created as a result of an exception being emitted or thrown. The original cause is stored as the attribute EXCEPTION.
30.4 Reading the Body in a Filter
A filter sometimes has to look at the bytes of the request body before the route runs, e.g. to verify a signature or a checksum, while the route still reads the body afterwards. A request filter method can take an AsyncRequestBody argument for it, see Reading the Body Asynchronously. A filter that is not given arguments, an HttpServerFilter or a filter of a functional route, e.g. beforeReplacingAsync, gets the same body with AsyncRequestBody.of(request).
The following pre-matching filter rejects a request whose body does not match the checksum in its X-Content-SHA256 header:
A filter that reads the body
The route still reads the body
Note
AsyncRequestBody.of(request) is supported by the Netty server. A server that does not support it, e.g. a servlet server without support for it yet, throws an UnsupportedOperationException: a filter method can take an AsyncRequestBody argument there instead.
Each call of AsyncRequestBody.of creates an independent reader. Readers of the same bytes share their consumption state and synchronize access, including readers acquired through different wrappers. Acquiring another reader does not copy the bytes or allow the same bytes to be consumed twice: read a copy() for each reader that must leave the body for the route. The body is resolved when the read starts, so an earlier lookup does not hide a body replacement by another filter.
Nothing is read before the filter reads the body, so requests whose filters do not read it stream their body to the route as usual. A request that sends Expect: 100-continue is answered with 100 Continue when the filter reads the body: a filter that answers without reading it, e.g. with 401, never receives it.
The copy is held in memory for the route, so it is limited by micronaut.server.max-request-buffer-size, and by the smaller limit given to bytes: a larger body fails the returned stage with a ContentLengthExceededException, answered with 413 Request Entity Too Large. What the reads of a body from AsyncRequestBody.of leave open is released when the request ends.
Register an implementation as a bean. The findHttpHeaders method receives the current HttpRequest and HttpResponse and returns the headers to add:
A ResponseHeaderPopulator example
The HTTP server invokes each ResponseHeaderPopulator for every response. Return null when no headers apply to the request and response. If the response already contains a returned header, its existing value takes precedence.
ResponseHeaderPopulator extends Ordered. If multiple populators are registered, implement getOrder() to control their invocation order.
Micronaut HTTP server version 4.5.0 introduced a new, more advanced API to access the bytes of an incoming request.
When an HttpRequest implements ServerHttpRequest, the new byteBody()
method returns a ByteBody with buffered, reactive and blocking APIs.
When an HTTP request comes in, it starts out with an unparsed and unclaimed stream of bytes as its byteBody(). After
all request filters have run, typically an argument binder matching the @Body parameter of the controller will
"claim" the byteBody() and e.g. parse the JSON. Finally, the body is closed at the end of the request lifecycle,
discarding any data if it has not been claimed by the argument binder.
Note
The FilterBodyParser API allows you to get a Map representation of a request’s body whose content type is either application/x-www-form-urlencoded or application/json.
ByteBody itself does not offer direct access to the data. To begin processing, there must be a primary
operation that converts the body into another form that can be used in the application programming model.
A normal ByteBody has two groups of streaming primary operations. toInputStream() gives access to the body
as a regular InputStream. The toByteArrayPublisher() and toByteBufferPublisher() methods return a reactive stream
of byte arrays or `ByteBuffer`s.
Warning
InputStream is blocking API, and the netty event loop must never be blocked. If you wish to read from the
body using an InputStream, take care to do so only on another thread, or to annotate your filter with
@ExecuteOn(TaskExecutors.BLOCKING).
If you need full access to the body, the buffer() method returns a CompletableFuture that completes with an
AvailableByteBody when the full body has been received. AvailableByteBody has a few more convenient
primary operations: toByteArray(), toByteBuffer() and toString(Charset).
Buffering is limited to the number of bytes configured in the micronaut.server.max-request-buffer-size property.
31.2 Splitting
Because the framework will not buffer the whole body in memory by default, after an ByteBody has been claimed (a
primary operation has been performed), the data is "gone", and the same ByteBody cannot be claimed again. That
means that if a filter were to claim the ServerHttpRequest.byteBody() directly (e.g. to print it to a log),
controllers could not access it anymore. The argument binder for the @Body argument would throw an exception.
To resolve this exclusivity problem, an ByteBody can be split before it is claimed. The split operation
essentially duplicates the body stream so that the two consumers (logging and argument binding) can process it
independently. A body can be split any number of times, but only before the primary operation.
While ServerHttpRequest.byteBody() returns a normal ByteBody — cleanup is done by the HTTP server if the
body is not consumed—the body returned by split is a CloseableByteBody. The caller must ensure that the
new instance is closed, otherwise there can be resource and memory leaks, stalled connections, or other issues.
Backpressure
When there are two consumers of the same stream of input data, the problem of backpressure coordination necessarily
comes up.
Backpressure in an HTTP server describes the behavior when the "downstream" consumers cannot consume data as fast as
the "upstream" supplier (i.e. the HTTP client sending the request) is sending it. To avoid having to buffer large
amounts of incoming data, the server will apply backpressure (make the client send its data more slowly) when
downstream consumers cannot keep up.
A split operation now introduces two consumers. Depending on use case, different approaches of dealing with the
backpressure of each downstream consumer may be appropriate. For example, if the two consumers write the body data to
two separate files at the same time, it’s best to use the backpressure of the slowest consumer to avoid buffering data.
But in another example, when one consumer is a filter that needs access to all the body data, and the other consumer
is the controller, the filter needs to complete before the controller even reads any data, so we should instead be
guided by the fastest of the two consumers.
These two approaches are already the two most important
SplitBackpressureModes. The full list of options is as follows:
SplitBackpressureMode.SLOWEST uses the backpressure of the slowest of the two consumers (first example)
SplitBackpressureMode.FASTEST uses the backpressure of the fastest of the two consumers (second example)
SplitBackpressureMode.ORIGINAL uses the backpressure of the original consumer (the one split() was called on)
SplitBackpressureMode.NEW uses the backpressure of the new consumer (the one split() returns)
The argument-less split() method uses SLOWEST, but you should pick the mode that is most appropriate for your use
case.
Warning
SLOWEST only works when both bodies are read at the same time. If one body is read to completion before
the other is read at all — the typical filter pattern of buffering a copy of the body and then letting the controller
read the original — SLOWEST deadlocks: the body that is not read yet never asks for data, so the server stops
reading after its initial window (about 64 KiB), the buffering consumer never completes, and the request hangs until
the idle timeout. A request with Expect: 100-continue does not even get its 100 Continue response. Use
split(SplitBackpressureMode.FASTEST) for this pattern.
The rule for choosing a mode:
Both bodies are consumed concurrently: SLOWEST.
One body is consumed (e.g. buffered) before the other: FASTEST. The other body buffers the data until it is read.
One body may not be consumed at all: close it, or call allowDiscard(), so that it does not hold back the other.
Discarding
Some consumers end up not needing the body after all. For example, if a POST request cannot be matched to a
controller route, the body is not needed and can be discarded. How discarding is implemented in the server depends on
HTTP version. For HTTP/1, the server might close the connection or simply drop the data (which can still save some
decompression overhead). For HTTP/2 the server can close the input of the request stream, instructing the client to
send no more data.
When there are multiple consumers, discard behavior is dependent on use case. In the above scenario of an unmatched
request, when there is a filter that is also subscribed to the body data, it may be appropriate to drop the request in
some cases (e.g. for logging) but may be necessary to still receive all the data in others.
To signal that the upstream may discard the body, you can call ByteBody.allowDiscard(). Only if all consumers
call allowDiscard() (or close() without a primary operation) may the remaining data actually be discarded. Before
that, all consumers, even those that called allowDiscard(), will still receive all data. For the logging use case,
you can call allowDiscard() and be assured that you will still log the full body if the controller needs it.
31.3 Trailers
The trailers of an HTTP message are header fields that follow the last bytes of the body: the trailer section of a
chunked HTTP/1.1 message, or the headers frame that ends an HTTP/2 stream. gRPC for example reports the status of a
call in the grpc-status response trailer.
The trailers travel with the body. ByteBody.trailers() returns a CompletionStage that
completes once the body has ended, with empty headers if the message carried no trailers. It is not a primary
operation, so it can be called before the body is claimed, and the bodies returned by split() and move() share it.
Buffering the body with buffer() does not keep the trailers, so read trailers() first when they are needed:
To send trailers, create the body with ByteBodyFactory.withTrailers(body, trailers).
The trailers are sent when the given stage completes, so they can be computed while the bytes are being sent, for
example a checksum. A message with such a body always uses the chunked transfer coding on HTTP/1.1, because trailers
cannot follow a body with a Content-Length. The same holds for a relayed body whose trailers have already arrived,
for example one that was received fully before it is sent on: its length is known, but the message is still chunked
so that the trailers are not lost. A route sends trailers by returning a
ByteBodyHttpResponse with such a body, and the raw HTTP client sends the trailers of the
request body it is given.
The Netty server, the Netty client and the raw client relay support trailers on HTTP/1.1, HTTP/2 and HTTP/3. A relay
that strips the hop-by-hop headers with HttpHeadersUtil.stripHopByHopHeaders keeps TE: trailers, so that the next
hop knows the trailers reach the client. The
JDK-based HTTP client neither sends nor receives trailers: it completes the trailers of a response empty, and it sends
a request body without its trailers.
31.4 Limiting and replaying
ByteBodyFactory has two methods that wrap a streamed body without buffering it first.
limit(body, maxBodySize) returns a body with the same bytes that fails with a
ContentLengthExceededException once more than maxBodySize bytes arrive. A body
whose known length is over the limit fails right away, and one whose known length is within the limit is returned
as is. Use it to apply a size limit to a body that is relayed rather than read, for example a different limit per
route.
replayable(body, maxBufferSize) returns a ReplayableByteBody. Each call to next()
returns a body with all the bytes from the start, so a body can be sent again after a failed attempt while it is
still arriving. The first reader streams the bytes and up to maxBufferSize of them are kept for the next readers.
Once more bytes arrive, the kept bytes are released: readers that already subscribed still get every byte,
isReplayable() returns false and next() throws a BufferLengthExceededException.
The bytes kept for a body from next() that is not read yet count against the same limit, and a reader holds at most
maxBufferSize bytes it has not consumed yet. A body whose known length is over the limit keeps nothing and is read
once. A body whose bytes are all available is replayable whatever its size. Close the ReplayableByteBody once no more
attempts are made, to release the kept bytes, and close every body from next() that is not read.
31.5 Example
This example adds a filter that will log the body bytes as they come in.
The @Body Person person parameter is the final consumer of the ServerHttpRequest.byteBody(). The argument
binder will internally perform a primary operation on the body, parse the JSON, and convert it to the Person object.
<2> The logBody filter will be called before the controller. However, it is programmed asynchronously, so the actual
logging may happen later as data is received.
<3> split the body so that we can work with it without interfering with the argument binder in <1>. We use SLOWEST
mode to prevent buffering: We don’t want to overwhelm the controller with data because the logging is usually very
fast, but at the same time we don’t want to overwhelm the logging if it is unexpectedly slower than the controller.
<4> The newly split body is in a try-with-resources statement to ensure that it is properly closed and there is no data
leak.
<5> We call allowDiscard() to signal that if the controller does not need the body after all, the logging filter is
fine with dropping it entirely. Without this call, the full body would always be logged, even if the body is discarded.
<6> Convert our copy of the body to a project reactor stream of byte[].
<7> Since we called allowDiscard(), there may be a BodyDiscardedException if the upstream decides that the body can
be dropped. We ignore that exception.
<8> Finally, subscribe to the reactive stream, and log any incoming data. Note that subscribe is asynchronous: It
will return immediately and then call the lambda with the log statement as data comes in.
If you run this example, you should see log output like this:
16:29:30.562 [default-nioEventLoopGroup-1-3] INFO i.m.docs.server.body.BodyLogFilter - Received body: eyJmaXJzdE5hbWUiOiAiSm9uYXMiLCAibGFzdE5hbWUiOiAiS29ucmFkIn0=16:29:30.604 [default-nioEventLoopGroup-1-3] INFO i.m.d.server.body.BodyLogController - Creating person Person[firstName=Jonas, lastName=Konrad]
With a short body like this, the log will only show one "packet". With more packets, the log statement will be called
multiple times:
16:29:30.562 [default-nioEventLoopGroup-1-3] INFO i.m.docs.server.body.BodyLogFilter - Received body: ...16:29:30.584 [default-nioEventLoopGroup-1-3] INFO i.m.docs.server.body.BodyLogFilter - Received body: ...16:29:30.642 [default-nioEventLoopGroup-1-3] INFO i.m.docs.server.body.BodyLogFilter - Received body: ...16:29:30.773 [default-nioEventLoopGroup-1-3] INFO i.m.d.server.body.BodyLogController - Creating person Person[firstName=..., lastName=...]16:29:30.708 [default-nioEventLoopGroup-1-3] INFO i.m.docs.server.body.BodyLogFilter - Received body: ...
Note that the logging in the above example is asynchronous, so the log statements may be interleaved as shown.
Functional routes let you handle HTTP requests with functions instead of controller methods. Define an HttpRoutes bean and use the HttpRouteBuilder it receives to declare routes:
Handler routes use the same HTTP server pipeline as controller routes. They support argument binding, body readers and writers, content negotiation, application filters, CORS, error handling and executor selection. A GET route also gets an implicit HEAD route.
Use functional routes to organize handlers into groups, apply filters and conditions in code, or route requests to a target found at runtime. You can implement HttpRoutes in a class or return it as a lambda from a factory method.
Functional routes coexist with controllers and use micronaut.server.context-path when configured. Route matching determines which route handles a request; the order of the HttpRoutes beans does not give their routes priority.
Declare routes and configure their builders inside HttpRoutes.routes(…) and group callbacks. Routes are fixed when the router is created; retaining a builder does not let you change them at runtime.
32.1 Declaring Routes
Build a route in this order:
Choose an HTTP method and URI, such as POST("/items").
Apply settings, such as media types or an executor.
Optionally choose a body stage, such as body(Item.class).
Finish with handle(…), handleAsync(…) or respond(…) to register the route.
For a simple route, GET(uri, handler) is a shortcut for GET(uri).handle(handler).
Declaring handler routes
Choosing a Method
Use GET, POST, PUT, PATCH or DELETE for common methods, or route(HttpMethod, uri) for another standard method. Use any(uri) to handle every method, including custom ones.
A specific-method route takes precedence over any(…) when the routes otherwise match equally well. More specific paths still take precedence: any("/files/special") can win over GET("/files/{name}").
GET also handles HEAD. Method mismatches produce 405 with an Allow header, and CORS works as it does for controllers. A path matched by any(…) accepts every method and does not produce 405.
Returning a Response
Return an HttpResponse from handle, or a CompletionStage of one from handleAsync. Exceptions go through the error routes, including checked exceptions. A synchronous handler returning null behaves like a controller returning null.
By default, the response writer uses the body’s runtime type. Use responseType(Class) or responseType(Argument) when the writer needs a declared type, including generic information such as Argument.listOf(Item.class).
Finish each route with exactly one terminal call. An unfinished route fails application startup. Configure settings before the terminal, and complete grouped routes before the group callback returns.
32.2 Routes in the Main Method
An application with only a few routes can declare them in its main method. HttpApplication starts the application, like Micronaut.run(args), and registers the routes as an HttpRoutes bean:
Routes in the main method
The rest of the application is discovered as usual. Controllers, filters, error handlers and configuration apply to these routes too, and the command line arguments are read as for Micronaut.run.
Using Beans
Routes declared outside a bean receive no injection. To use beans, take a second callback argument, a BeanLocator:
Routes that use beans
Choosing and Configuring the Server
Use HttpApplication.build(args) to configure the application before it starts:
Choosing the server
Use micronaut(…) to reach the underlying Micronaut launcher, for example to set environments or property sources.
Note
server only chooses which server is started. A bean that injects EmbeddedServer still receives the server that the bean context chooses, which can be another server on the classpath. When several servers are on the classpath, inject the chosen type instead.
32.3 Responses Without a Handler
Use respond(…) for a fixed response, a redirect, or an answer that depends only on path variables. Use a handler when you need headers, query parameters, cookies, the body or other request data.
Routes that respond without a handler
Choosing a Response Form
Form
Use for
respond(HttpResponse)
A fixed status, headers and reusable body.
respond(Supplier<HttpResponse>)
A fresh response for each request.
respond(Function<PathVariables, HttpResponse>)
An answer computed from path variables.
Apply route settings and filters before respond, as you would before handle.
Response Ownership
A fixed response gets a fresh response object for every request, so filters can safely change its headers. Its body object is shared: use an immutable or otherwise reusable body. For a body consumed during writing, such as a buffer or single-use publisher, use a supplier that creates it for each request.
Suppliers and functions must return a fresh response each time.
Media Types and Execution
These routes do not read the request body and accept any request content type. A fixed response with a content type uses that as the route’s produced type; otherwise, the group’s produced types apply.
Fixed responses default to event-loop execution rather than inheriting the group’s executor. Suppliers and functions use the route’s or group’s executor, or the event loop if none is selected. Use executeOn(…) if computing the response blocks. Exceptions use the normal error routes.
32.4 Reading the Request Body
Choose a body stage according to how the handler needs to read the request:
Stage
Handler receives
body(Class) or body(Argument)
A decoded body, once it has fully arrived, like a controller’s @Body argument.
An AsyncRequestBody that the asynchronous handler reads as needed.
No body stage
Only the request and path variables. The handler does not read the body.
Returning a CompletionStage does not make a typed body stream: both handle and handleAsync receive the decoded body after it has arrived.
Reading the body in asynchronous handlers
Reading and Releasing the Body
An AsyncRequestBody supports one read. Choose one of body, text, bytes, elements, transferTo, form, parts, takeBody or discardBody. A second read fails with IllegalStateException.
The server discards unread content and releases resources when the handler’s stage completes. Keep that stage pending until your body processing finishes. If you call takeBody(), ownership transfers to you: close the returned body yourself. Use discardBody() when you want to drop it explicitly.
Reads see the body left by filters. They do not populate request.getBody() or change the byte-body access contract. Error routes do not receive an AsyncRequestBody, because a failure may occur after it has been consumed.
Limits and Threads
Collected bodies are bounded by micronaut.server.max-request-buffer-size. text(int) and bytes(int) use the smaller of that setting and the supplied limit.
Streaming methods avoid buffering the whole body, but individual decoded elements and text fields still have buffer limits. The overall request remains subject to micronaut.server.max-request-size.
An asynchronous read may complete on the event loop. Do not block its continuation. Propagated request context is not automatically in scope there; use a context-propagating executor if needed, or a typed body stage whose handler runs after decoding.
In located routes, body handlers also receive the target: (request, pathVariables, target, body) → ….
32.5 Forms and File Uploads
Functional routes support both application/x-www-form-urlencoded and multipart/form-data. Use form() to collect a form before the handler runs, or body() to read it yourself, including streaming parts as they arrive.
Forms and file uploads
Upload Limits and Cleanup
The normal multipart configuration applies. micronaut.server.multipart.max-file-size limits each file, and micronaut.server.max-request-buffer-size limits collected text fields. Large files can be stored on disk according to the multipart settings.
The server deletes temporary files still owned by the form when the request ends. Call FormData.close() to release them earlier. transferTo(Path) never replaces an existing file, and a failed or aborted transfer leaves no partial destination file.
Tip
Generate or sanitize server-side file names. Never use an uploaded file name directly as a destination path.
FormData and FormParts can also be bound as controller method arguments.
32.6 Server-Sent Events and Streamed Responses
Functional routes support two streaming styles without a Publisher:
sse(…) supplies an SseEmitter for pushing server-sent events.
An BodyElements response body supplies elements that the server pulls as the client is ready for them.
You can return BodyElements read from a request or HTTP client directly as a response body.
Server-sent events and streamed responses
Starting and Ending an Event Stream
The first event, comment or heartbeat sends the response headers: 200, text/event-stream and Cache-Control: no-cache. Until then, you can add headers, use respond(…), or fail through the normal error routes. Event streams are not compressed.
For callback-driven output, call keepOpen() before the handler returns and arrange to call complete() or fail(…) later. Use onClose to stop upstream work. A comment or per-stream heartbeat can send headers early while waiting for the first event.
Slow Clients and Threads
Each send returns a stage that reflects the connection’s capacity, allowing for buffered bytes up to a high-water mark. Chain sends or wait on a worker executor. Sending indefinitely without waiting can overflow the buffer and fail the stream; isWritable() lets you drop or coalesce events instead.
Disconnects fail pending and later sends with ConnectionClosedException. Send continuations and onClose usually run on a server event loop, with propagated context. Keep them non-blocking.
Configure the high-water mark and a default SSE heartbeat with:
The configured heartbeat starts after the response has been sent. To start an idle stream before its first event, send a comment or set a heartbeat on the emitter itself.
Streaming Elements
The server reads BodyElements one element at a time, writes them with the normal body writers and closes them when the response ends or is abandoned. This includes disconnects and responses replaced by filters. A HEAD request sends headers without streaming the body; for SSE, the handler still runs.
For asynchronous cleanup, use BodyElements.ofAsync(cursor::nextAsync, cursor::closeAsync). closeAsync() lets an owner await cleanup; close() starts it without waiting. If you consume elements yourself, close them when finished, including after the final empty element. When returned as a response body, the server manages cleanup.
Kotlin Coroutines
Server-sent events and streamed responses with coroutines
32.7 Route Groups and Path Prefixes
Use routes.path(prefix, group → …) to group routes under a path prefix, or routes.group(group → …) to share settings without adding a prefix. Groups can contain nested groups.
A group with a prefix and filters
Shared Settings
Group settings provide defaults for all routes in the callback, including nested groups. A route or nested group overrides the settings it declares itself.
Group filters run only after one of the group’s routes matches. They do not cover unmatched requests under the prefix, including 404, 405, 415 and 406 responses. Use a server filter for those requests.
A form route consumes the form media types regardless of its group’s consumed types. Error and status routes do not inherit group media types or executors. Located routes inherit group defaults and can override them.
Complete group configuration inside its callback. To apply route filters to every route of an HttpRoutes bean, wrap those routes in routes.group(…).
32.8 Route, Group and Server Filters
Choose the filter scope according to the requests it should cover:
Route filters apply to one matched route: routes.GET(uri).before(…).and().handle(…).
Group filters apply to matched routes in a group and its nested groups.
Server filters, declared with routes.serverFilter(patterns…), apply to all matching request paths, including controller routes, static resources and unmatched requests.
Use a server filter when you need to cover responses such as 404, 405, 415 or 406 that occur before a route is selected. Only the top-level builder declares server filters.
Server filters
Filter Methods
Method
Behaviour
before(…)
Change the request in place and continue.
beforeReplacing(…)
Return a response to stop processing, a replacement request to continue, or null to keep the current request.
after(…)
Change the response in place.
afterReplacing(…)
Return a replacement response, or null to keep the current response.
Each method has an asynchronous variant, such as beforeAsync or afterReplacingAsync, that returns a CompletionStage. The chain continues when that stage completes.
Overloads can receive MutablePropagatedContext, for example before((request, context) → …). Added context is available to subsequent filters, the handler and error routes. Asynchronous context changes take effect when the stage completes.
Choosing an Executor
Filters normally run on the filter-chain thread. Use executeOn(…) for blocking work, then and() to continue configuring the route or group:
A filter on an executor
Note
Python uses trailing underscores for Java methods named after keywords: and_() and or_(…).
Changing Requests and Responses
Use before for in-place changes to headers, attributes or the URI. Use beforeReplacing to supply a replacement request, including one with a different method or body. Replacing the raw body requires a ServerHttpRequest with the replacement byteBody().
Only a pre-matching server filter can change which route is selected. A later URI or method change does not trigger routing again.
Use after to update response headers or other mutable properties, and afterReplacing to return an entirely different response.
Filter Order
Request processing proceeds through:
Server filters, including @ServerFilter beans, sorted together by order. Pre-matching filters run before routing.
Outer group filters, then nested group filters.
Route filters.
The handler.
Responses pass back through the scopes in reverse order. Within each route or group, request filters and response filters each retain declaration order. Response filters also see responses returned early by request filters at their own or an inner scope.
Filter failures use the normal error routes and exception handlers.
32.9 Conditions, Order, Ports and Attributes
Request Conditions
Use where(…) on a route or group to match requests by headers, query parameters, cookies, method, host, address or time. Combine RequestCondition values with all, any, and, or and not.
A route for the requests that meet a condition
A rejected condition removes the route from consideration. Another route can handle the request; if none matches, the response is 404. Conditions run during routing, so keep them fast, non-blocking and free of side effects. Use the dedicated body conditions below to inspect a body.
ValueMatcher provides comparisons such as equalTo, startsWith, oneOf, regex and present. Matching is case-sensitive unless you use ignoringCase(), and regex matches the whole value. To test for a missing value, use present().negate().
Conditions with matchers
host(…) uses the server’s HttpHostResolver, including its forwarded-host configuration. For custom checks, use where(request → …) or RequestCondition.custom(…).
Important
Use peerAddress(…) for checks against the actual connection address. remoteAddress(…) uses the server’s client-address resolver and can read forwarded headers. Use it for access control only behind a trusted proxy that sets the configured client-address header.
Route Order
order(int) breaks ties between otherwise equally suitable routes. Lower values win; the default is 0. It does not override media-type matching or URI specificity. Among equally specific matches, a method-specific route wins over any(…), and an explicit HEAD route wins over an implicit one before order is considered.
Order applies across groups, HttpRoutes beans and controllers. A route’s order overrides its group’s default. Ordering an HttpRoutes bean itself does not give its routes priority. If multiple routes remain tied, the request is ambiguous and produces 400.
Body Conditions
Use RequestCondition.body(…), bodyText(…) or bodyBytes(…) to choose between routes based on body content.
Routes chosen by the body of the request
The server first matches the path, method, media types and other conditions. It then checks body conditions in route-preference order. Multiple equally preferred matches remain ambiguous. A failed asynchronous condition counts as a non-match and is logged. Keep predicates and asynchronous lookups fast.
Body conditions can be combined with all(…) or and(…), but cannot appear inside any(…), or(…) or not(…). They cannot be combined with a custom route selector.
Important
Body conditions buffer the body, even if the selected handler would otherwise stream it. The handler can read that body afterwards. Use these conditions for bounded request bodies.
maxBytes(…) limits a condition’s read; the default is micronaut.server.max-request-buffer-size. For routes known at startup, an explicit limit above that server setting fails startup. Configure limits on dynamically located routes carefully, since those routes are built later.
A body that cannot be decoded or exceeds a condition’s limit does not match it. If buffering itself exceeds the server limit, the request can fail with 413; do not rely on a streaming fallback to accept an oversized body. If no route’s body conditions match, the response is 404.
Path Variable Constraints
Use constrain(…) to restrict path variables before a route is selected:
Routes for some values of a path variable
Constraints accept predicates, collections of allowed strings or ValueMatcher values. A missing or unconvertible value, or a predicate that throws, rejects the route. Group and route constraints must all pass. Keep them fast and non-blocking.
Route Attributes
attribute(name, value) adds metadata to a route or every route of a group. Handlers and filters read it from the matched RouteInfo, available through RouteAttributes.getRouteInfo(request) or a filter’s RouteInfo parameter.
Route attributes
These are route attributes, separate from request attributes. Their map is read-only and does not allow null values.
Ports
port(…) restricts a route or group to a port and causes the server to open that port:
Routes on another port
Use an integer or a property expression, such as port("${my.admin.port:8081}"). Values must resolve to a port from 1 to 65535; invalid values fail startup.
Once routes declare dedicated ports, routes without a port match only the server’s default ports.
32.10 Annotations on Handler Routes
A lambda has no method annotations. Use annotate(…) when a route needs metadata for features such as filter matching, API versioning, CORS, executor selection or Micronaut Security.
For example, annotate(Audited.class) adds an annotation without members, and annotate(Version.class, version → version.value("2")) supplies a value. You can also pass an AnnotationValue and chain calls to add more annotations.
Annotations on a group apply to its routes and nested groups. More specific declarations override the annotation members they set.
An annotation that binds a filter
Handler routes with annotations
The /prices route has no filter annotation, so the filter does not apply to it.
Reusing Bean Metadata
Use annotationMetadata(provider) to reuse an annotated element’s metadata. An ExecutableMethod also identifies the bean method represented by the route, enabling integrations such as local @Error handlers. A bean definition supplies annotations only.
The handler still determines the arguments, execution and result. Supplying metadata does not invoke the bean method in place of the handler.
For framework integrations, a handler can implement RouteMethodProvider to supply its ExecutableMethod. An explicit annotationMetadata(…) on the route takes precedence. Lambdas do not infer a bean method automatically.
Runtime Metadata Limitations
Annotations added at runtime do not automatically include their meta-annotations. Features that look up the annotation directly can use it; features that depend on a stereotype need that stereotype supplied through AnnotationValueBuilder.stereotype(…).
Annotation expressions require compile-time processing. Use where(…) for handler-route conditions instead of adding @RouteCondition dynamically.
32.11 Error and Status Routes
Use error(type, handler) to handle an exception type and its subtypes, and status(status, handler) to handle a response status. The asynchronous variants, errorAsync and statusAsync, return a CompletionStage.
The declaration determines the scope:
On the top-level builder, handlers are global: they apply to controllers and functional routes, like @Error(global = true).
Inside a group, handlers are local: they apply to that group’s routes and nested groups.
Error and status routes
For a failed handler route, the server looks for an error handler in this order:
The innermost route group, then enclosing groups, including locator groups.
Global functional error routes and @Error(global = true) methods.
Within each level, the closest exception type wins. Local scope takes precedence over a more exact global match. Status routes use the same scope order, including for HttpStatusException.
Requests that match no route use only global handlers. A group’s local status route therefore does not handle every 404 under its prefix.
Error handlers receive the request and the filters' propagated context. Route and group response filters also apply to their responses. If an error handler fails, the server returns its default error response.
32.12 Locator Routes
A locator routes a path prefix to a target found at request time, such as a shop selected by a path variable. An LocatedRoutes declares that target’s routes relative to the prefix. Its handlers receive the target as an additional argument.
Locator routes
Matching and Execution
The locator runs at most once per request. A null target produces 404; exceptions use normal error handling, starting with the locator’s groups.
Once a target is selected, routing continues within its routes. If none matches, the response is 404, 405, 415 or 406 as appropriate. The request does not fall back to unrelated application routes.
Synchronous locators run during route matching, typically on the event loop, and must not block. Use locateAsync for asynchronous work. The server stops waiting if the client disconnects, without cancelling the returned stage, which may be shared.
Groups and Settings
Located routes inherit filters, annotations, attributes, media types and executors from the groups enclosing the locator. Their own settings and groups can override these defaults. Filters run from the enclosing locator groups inward to the matched route.
Located routes can contain further locators. They cannot declare server filters, global error or status routes, dedicated ports, WebSocket routes or static resource routes. Declare those on the application builder instead.
Locators That Read the Body
Use locator(prefix) followed by a body stage when finding the target requires request data:
A locator that reads the form
Body-reading locators use a copy of the body and buffer it within micronaut.server.max-request-buffer-size. Larger bodies produce 413. A decoded body is required unless its type is nullable.
Note
Form locators require a server that supports decoding forms from a copy of the request bytes, such as Netty. They are not supported on servers without that capability, such as servlet containers.
32.13 Route Templates and Route Selectors
Most applications should use the default URI templates and route selection. The experimental APIs in this section support integrations with other web frameworks that need different matching rules.
Custom Template Languages
Call templates(parser) on a group to provide an RouteTemplateParser. It parses route paths and locator prefixes into RouteTemplate instances.
Custom Route Selection
Use selectedBy(selector) on a group or route to provide an RouteMatchSelector. The selector runs when all matching candidates share the same selector instance. Otherwise, Micronaut’s normal matching policy applies.
A selector can also negotiate the response media type. Handlers can read it with RouteMatchSelector.selectedMediaType(pathVariables).
By default, the router removes candidates with incompatible media types before invoking the selector. An integration that must choose by path first can return true from selectsMediaTypes() and report Selection.notAcceptable(match) or Selection.unsupportedMediaType(match) for 406 or 415. Returning no selection instead means no route was found.
For locator precedence, a selector can provide an order() comparator. It lets a more specific route take precedence over a locator of the same selector, including when determining allowed methods.
See the API documentation for the full selection contract. Custom selectors cannot be combined with request-body conditions.
32.14 WebSocket Routes
Finish a GET route with webSocket(…) to declare WebSocket handlers instead of an HTTP response handler. Route groups, filters, conditions, order, ports and attributes apply to the upgrade request.
Handlers receive the session first and return a CompletionStage when work completes later. For example, return the stage from session.sendAsync(…). Available callbacks include onOpen, onMessage, onMessageStream, onPing, onPong, onClose and onError.
Handlers run on the event loop by default. Use executeOn(TaskExecutors.BLOCKING) for blocking work. Store per-connection state in session attributes, since handler instances are shared.
Sessions work with session.getOpenSessions() and WebSocketBroadcaster alongside annotated WebSockets. Inject the broadcaster through BeanProvider<WebSocketBroadcaster> to avoid a startup dependency cycle.
Upgrade Requests
Application, group and route filters run on the upgrade request. Returning an HTTP response rejects the upgrade:
A filter that rejects the upgrade
Conditions and route order also apply. Among functional WebSocket matches, the most specific route wins, then the lowest order. Functional WebSocket routes take precedence over @ServerWebSocket beans.
Declare WebSocket routes on the application builder, not in located routes. They have no HTTP body media types or response type: group media types are ignored, and consumes, produces and responseType are not supported. Set the message type on the message handler instead.
Message Streams
session.sendAllAsync(publisher) sends a publisher’s messages at the connection’s pace. Its stage completes with true after all messages are sent, or false if the session closes first; closing cancels the publisher.
Use onMessageStream(…) to receive a publisher of incoming messages:
Streams of messages
Choose either onMessage or onMessageStream. Stream handlers follow subscriber demand and do not support maxConcurrentMessages.
Delivery and Backpressure
By default, handlers run sequentially per connection: the next message waits until the previous handler’s stage completes. The server reads ahead up to maxPendingMessages, 16 by default. Fragmented messages count each fragment toward that limit.
Concurrent handlers
When the pending limit is reached, message decoding pauses until handlers catch up. maxPendingMessages(0) disables this backpressure, so use it carefully.
Important
A handler’s stage delays subsequent messages. To send an ongoing stream while continuing to receive messages, start the stream and return null rather than returning a stage that never completes.
These delivery settings apply to functional WebSocket routes. Annotated @ServerWebSocket beans retain their existing behaviour.
32.15 Static Resources
Finish a GET route with resources(…) to serve files or classpath resources under a URI prefix. Use StaticResources for the standard file-serving behaviour.
Serving static resources under a prefix
The prefix itself serves the index file, and paths beneath it serve individual resources. GET also registers HEAD. More specific controller or functional routes under the prefix take precedence.
Static resources in a group
Resource Options
StaticResources is immutable; each option returns a configured copy.
Option
Purpose
classpath(base), fileSystem(directory)
Choose a classpath location or existing filesystem directory.
of(locations…)
Search multiple locations in order, using values such as classpath:public or file:/var/www.
indexFile(name)
Choose the directory index file, index.html by default. Use null to disable it.
cacheControl(value)
Set the Cache-Control header.
pathVariable(name)
Rename the resource path variable, path by default, if it conflicts with a prefix variable.
hiddenFiles(true)
Allow paths with dot-prefixed segments, which are hidden by default.
Serving the hidden files
Responses and Paths
Resources use normal file responses: content types follow extensions, Last-Modified supports conditional requests with 304, and HEAD returns headers without content. Byte ranges produce 206 for filesystem-backed files; resources inside jars do not support that range handling.
Missing resources and directories without an index return 404. Unsafe or alternative path spellings are also rejected, including traversal, encoded separators, repeated separators and incorrect filename case on case-insensitive filesystems. Use the resource’s exact path.
Resource routes must use GET and standard URI templates. Prefix variables must match a single non-empty path segment, and cannot use the resource handler’s path-variable name. Declare resource routes on the application builder, not in located routes.
Reading files and jars blocks. Keep the default blocking executor or choose another suitable executor. Group filters, conditions, annotations and other route settings apply, while group media types do not.
Choosing Configuration or Routes
Use static resource configuration when you want to serve files without code or choose locations per environment. Use a functional resource route when you need route-specific filters, conditions, caching headers or grouping with other routes.
32.16 Limitations
Functional routes are experimental. Keep these limitations in mind:
Routes and their settings are fixed when the router is created. Configure them inside HttpRoutes.routes(…) and group callbacks; do not retain builders to make runtime changes.
Tools that discover routes from controller annotations, such as Micronaut OpenAPI, do not discover handler routes.
There is no Kotlin DSL or support for suspend handler functions. The streaming APIs do provide coroutine helpers.
Validation
Handler bodies and path variables are not automatically validated. To validate them, call a bean method whose parameters carry constraints such as @Valid or @NotBlank. A constraint violation produces 400, as it does for a controller.
A bean method that validates its parameters
A handler route that validates its body through the bean
33 HTTP Sessions
See the documentation for Micronaut Session for information about supporting HTTP sessions in your applications.
To emit news headline events, write a controller that returns a Publisher of Event instances using whichever Reactive library you prefer. The example below uses Project Reactor's Flux via the generate method:
Publishing Server Sent Events from a Controller
Note
You typically want to schedule SSE event streams on a separate executor. The previous example uses @ExecuteOn to execute the stream on the I/O executor.
The above example sends back a response of type text/event-stream and for each Event emitted the Headline type previously will be converted to JSON resulting in responses such as:
Server Sent Event Response Output
data: {"title":"Micronaut 1.0 Released","description":"Come and get it"} data: {"title":"Micronaut 2.0 Released","description":"Come and get it"}
You can use the methods of the Event interface to customize the Server Sent Event data sent back, including associating event ids, comments, retry timeouts, etc.
35 WebSocket Support
The Micronaut framework features dedicated support for creating WebSocket clients and servers. The io.micronaut.websocket.annotation package includes annotations for defining both clients and servers.
Warning
Since Micronaut Framework 4.0. io.micronaut:micronaut-http-server no longer exposes micronaut-websocket transitively. To use annotations such as @ServerWebSocket, add the micronaut-websocket dependency to your application classpath:
The @ServerWebSocket annotation can be applied to any class that should map to a WebSocket URI. The following example is a simple chat WebSocket implementation:
WebSocket Chat Example
Note
Like the URI of a controller, the URI of a @ServerWebSocket is under the context path of the server, micronaut.server.context-path, if one is configured. With a context path of /app, the example above is mapped under /app/chat/{topic}/{username}.
Tip
A working example of WebSockets in action can be found at Micronaut Guides.
Tip
A WebSocket endpoint can also be declared in code, as a route of the route builder, with its handlers as functions, streams of messages and backpressure: see WebSocket Routes.
For binding, method arguments to each WebSocket method can be:
A variable from the URI template (in the above example topic and username are URI template variables)
The @OnClose method can optionally receive a CloseReason. The @OnClose method is invoked prior to the session closing.
The @OnMessage Method
The @OnMessage method can define a parameter for the message body. The parameter can be one of the following:
A Netty WebSocketFrame
Any Java primitive or simple type (such as String). In fact, any type that can be converted from ByteBuf (you can register additional TypeConverter beans to support a custom type).
A byte[], a ByteBuf or a Java NIO ByteBuffer.
A POJO. In this case, it will be decoded by default as JSON using JsonMediaTypeCodec. You can register a custom codec and define the content type of the handler using the @Consumes annotation.
A WebSocketPongMessage. This is a special case: The method will not receive regular messages, but instead handle WebSocket pongs that arrive as a reply to a ping sent to the client.
The @OnError Method
A method annotated with @OnError can be added to implement custom error handling. The @OnError method can define a parameter that receives the exception type to be handled. If no @OnError handling is present and an unrecoverable exception occurs, the WebSocket is automatically closed.
Using @ExecuteOn For Blocking Methods
The callback methods of a @ServerWebSocket are invoked on the main Netty server event loop by default. If you do any blocking I/O operations in your WebSocket handler methods, then you must offload those tasks to a separate thread pool in the same manner as with HTTP. As with a @Controller or @Filter, the @ExecuteOn annotation can be applied to a @ServerWebSocket handler at either the type or method level.
Non-Blocking Message Handling
The previous example uses the broadcastSync method of the WebSocketBroadcaster interface which blocks until the broadcast is complete. A similar sendSync method exists in WebSocketSession to send a message to a single receiver in a blocking manner. You can however implement non-blocking WebSocket servers by instead returning a Publisher or a Future from each WebSocket handler method. For example:
The example above uses broadcast, which creates an instance of Publisher and returns the value to Micronaut. The Micronaut framework sends the message asynchronously based on the Publisher interface. The similar send method sends a single message asynchronously via Micronaut return value.
For sending messages asynchronously outside Micronaut annotated handler methods, you can use broadcastAsync and sendAsync methods in their respective WebSocketBroadcaster and WebSocketSession interfaces. For blocking sends, the broadcastSync and sendSync methods can be used.
@ServerWebSocket and Scopes
By default, the @ServerWebSocket instance is shared for all WebSocket connections. Extra care must be taken to synchronize local state to avoid thread safety issues.
If you prefer to have an instance for each connection, annotate the class with @Prototype. This lets you retrieve the WebSocketSession from the @OnOpen handler and assign it to a field of the @ServerWebSocket instance.
Sharing Sessions with the HTTP Session
The WebSocketSession is by default backed by an in-memory map. If you add the session module you can however share sessions between the HTTP server and the WebSocket server.
Note
When sessions are backed by a persistent store such as Redis, after each message is processed the session is updated to the backing store.
Tip
Using the CLI
If you created your project using Application Type Micronaut Application, you can use the create-websocket-server command with the Micronaut CLI to create a class annotated with ServerWebSocket.
$ mn create-websocket-server MyChat| Rendered template WebsocketServer.java to destination src/main/java/example/MyChatServer.java
Connection Timeouts
By default, Micronaut framework times out idle connections with no activity after five minutes. Normally this is not a problem as browsers automatically reconnect WebSocket sessions, however you can control this behaviour by setting the micronaut.server.idle-timeout setting (a negative value results in no timeout):
This lets you use the same service discovery and load balancing features for WebSocket clients.
Once you have a reference to the WebSocketClient interface you can use the connect method to obtain a connected instance of a bean annotated with @ClientWebSocket.
For example consider the following implementation:
WebSocket Chat Example
You can also define abstract methods that start with either send or broadcast and these methods will be implemented for you at compile time. For example:
WebSocket Send Methods
public abstract void send(String message);
Note by returning void this tells the Micronaut framework that the method is a blocking send. You can instead define methods that return either futures or a Publisher:
WebSocket Send Methods
public abstract reactor.core.publisher.Mono<String> send(String message);
The above example defines a send method that returns a Mono.
WebSocket Send Methods
public abstract java.util.concurrent.Future<String> sendAsync(String message);
The above example defines a send method that executes asynchronously and returns a Future to access the results.
Once you have defined a client class you can connect to the client socket and start sending messages:
For illustration purposes we use blockFirst() to obtain the client. It is however possible to combine connect (which returns a Flux) to perform non-blocking interaction via WebSocket.
Connecting without Reactor
AsyncWebSocketClient is the counterpart of WebSocketClient whose connect methods return a CompletionStage that completes with the connected client endpoint. Inject it like a WebSocketClient, or obtain it from one with toAsyncWebSocket():
Each call connects once. Cancelling the future of the stage before the connection is established, or completing it otherwise, for example with orTimeout, aborts the connect; an endpoint that connects after that is closed. The stage may complete on an event loop thread, so its continuations must not block. They run with the propagated context of the caller of connect.
Cancelling or timing out a dependent stage created with thenApply, thenCompose, or another standard stage method leaves the original connect running. Keep the original future and apply a deadline to it when the deadline should abort the connect. Cancellation is a best-effort hint. Once connected, close the endpoint or its WebSocketSession when finished, including when abandoning the session after a later processing timeout. Cancelling the completed connect future does not close an active session.
With the AsyncWebSocketClient of the Netty client, a CompletionStage returned by an @OnOpen or @OnMessage method of the endpoint is awaited like a returned Publisher: the stage of connect completes once the stage of @OnOpen completed, and a failed stage reaches the @OnError method. The messages are still handled as they are read: the next message does not wait for the stage of the previous one. The stage of an @OnClose or @OnError method does not hold the close back. The reactive WebSocketClient treats such a stage as a plain value, as before.
The injected client, and the one returned by toAsyncWebSocket(), are views of the underlying HTTP client: closing them closes that client, which is shared with the other injection points of the same client. Outside of an application use AsyncWebSocketClient.create(uri), and close the client once done.
Tip
Using the CLI
If you created your project using the Micronaut CLI and the default (service) profile, you can use the create-websocket-client command to create an abstract class with WebSocketClient.
$ mn create-websocket-client MyChat| Rendered template WebsocketClient.java to destination src/main/java/example/MyChatClient.java
Handshake timeout and load balancing
By default a connect waits for the response to its upgrade request without a limit. Set handshake-timeout (micronaut.http.client.handshake-timeout, or the handshake-timeout of a service client) to fail a connect whose server does not answer the upgrade in time with a ReadTimeoutException; the connection is then closed. Cancelling the subscription to connect before the endpoint is emitted closes the connection.
The outcome of the handshake is reported to the load balancer that selected the instance, like the outcome of an HTTP exchange: a success once the instance responded to the upgrade with a status below 500, a server error for a status of 500 or above, a connect failure when the connection could not be opened, a timeout of handshake-timeout, and a reset when the instance closed the connection before it responded. A cancelled connect only releases the selection.
36 HTTP/2 Support
Since Micronaut framework 2.x, Micronaut Netty-based HTTP server can be configured to support HTTP/2.
Configuring the Server for HTTP/2
The first step is to set the supported HTTP version in the server configuration:
With this configuration, the Micronaut framework enables support for the h2c protocol (see HTTP/2 over cleartext) which is fine for development.
Since browsers don’t support h2c and in general HTTP/2 over TLS (the h2 protocol), it is recommended for production that you enable HTTPS support. For development this can be done with:
See the documentation on Tomcat Native for more information.
HTTP/2 Server Push Support
Support for server push has been added in Micronaut framework 3.2. Server push allows for a single request to trigger multiple responses. This is most often used in the case of browser based resources. The goal is to improve latency for the client, because they do not have to request that resource manually anymore, and can save a round trip.
A new interface, PushCapableHttpRequest, has been created to support the feature. Simply add a PushCapableHttpRequest parameter to a controller method and use its API to trigger additional requests.
Before triggering additional requests, the isServerPushSupported() method should be called to ensure the feature is available. Once it’s known the feature is supported, use the serverPush(HttpRequest) method to trigger additional requests. For example: request.serverPush(HttpRequest.GET("/static/style.css"))).
37 HTTP/3 Support
Since Micronaut framework 4.x, Micronaut’s Netty-based HTTP server can be configured to support HTTP/3.
Configuring the Server for HTTP/3
Instead of the TCP used for HTTP/1.1 and HTTP/2, HTTP/3 runs on UDP. To expose an HTTP/3 server, you need to define a listener with the special QUIC protocol family:
that defining this listener will disable the implicit TCP listeners. You can add them manually as described in the listener section.
The micronaut.server.idle-timeout setting also applies to HTTP/3 connections, as the QUIC maximum idle timeout (max_idle_timeout). Idle QUIC connections are closed once it elapses. A negative value disables the idle timeout.
Additionally, the netty HTTP/3 codec needs to be present on the classpath:
The HTTP server emits a number of Bean Events, defined in the io.micronaut.runtime.server.event package, that you can write listeners for. The following table summarizes these:
Emitted for every request once the server is done with it. This does not imply that a response was delivered.
Warning
Doing significant work within a listener for a ServerStartupEvent will increase startup time.
Warning
Listeners for HttpRequestReceivedEvent and HttpRequestTerminatedEvent are invoked on the thread the server selects for request handling, which with the default micronaut.server.thread-selection (MANUAL, and also with AUTO) is the Netty event loop of the connection. They must not block: a slow listener delays every other connection on that event loop. Annotate the @EventListener method with @Async to run it on an executor, or set micronaut.server.thread-selection to BLOCKING to move listeners (and controllers) off the event loop. With debug logging enabled for io.micronaut.http.server.netty.RoutingInBoundHandler, the server logs listeners that take longer than 100 ms on the event loop.
Callbacks of a single request
A listener of HttpRequestTerminatedEvent sees every request. A route that only needs to know about its own request, for example to record the outcome of the response or to stop work when the client goes away, registers a callback on the request with ServerHttpRequest instead. Nothing is done for the requests that register none.
Callbacks of a request
Register the callbacks before the response is handed to the connection, in the route, in a filter, or in a response filter. A registration after that is refused: whenResponseCompleted returns false and whenAbandoned returns null. They also do so on a server that does not support them.
The callbacks run on the Netty event loop of the connection, which also serves other connections. They must not block: completing or cancelling a future, counting a metric or logging is fine, but blocking IO (for example JDBC or a blocking HTTP call) and long CPU-intensive work must be handed to an executor, for example the TaskExecutors.BLOCKING executor, or they delay every other connection on that event loop.
Note
Both methods are experimental, and may change in a later release.
...@Singletonpublic class MyBean { @EventListener public void onStartup(ServerStartupEvent event) { // logic here ... }}
39 Training Runs
A JDK AOT cache (-XX:AOTCacheOutput), a dynamic CDS archive (-XX:ArchiveClassesAtExit), a GraalVM profile for profile-guided optimization (.iprof) and the native-image-agent configuration are all written when the JVM exits after a representative run of the application. A Micronaut application does not exit on its own, so the build would have to start it, wait until it is ready, send it some requests and stop it.
A training run does this from inside the application. It has two modes, which micronaut.application.training.mode selects:
start, the default, starts the application as usual, HTTP server included. Once the server has started, it can send warm-up GET requests to it. Then it stops the application and exits.
load starts neither the application nor the server: it reads the configuration, loads the bean definitions without creating any bean and exits. It sends no request. It is described in Training without starting the application.
Set micronaut.application.training.enabled to true, for example with -Dmicronaut.application.training.enabled=true or the MICRONAUT_APPLICATION_TRAINING_ENABLED=true environment variable. In the start mode, Micronaut.run then:
logs a warning that this JVM is a training run and does not serve traffic, before the application starts,
starts the application as usual and waits until startup has completed and every startup event listener has run,
logs a second warning, stops the application and closes the application context,
exits the JVM with status 0.
Only true turns the switch on, in any case (TRUE works too). Any other text, yes and on included, leaves it off when the value is a string: a system property, an environment variable, a .properties file or a quoted YAML value. An unquoted yes or on in a YAML file is different: YAML reads it as the boolean true, so enabled: yes in application.yml turns the training run on.
A failure during startup, the warm-up included, still exits with a non-zero status, so a failed training run fails the build. This applies to every embedded application, including messaging applications without an HTTP server. In the test environment the application is stopped, but System.exit is not called.
This is the start mode of a training run, the default. It creates the beans of the application, so it needs the services they use. If the application cannot start where the training runs, for example because it needs a database and the training is a step of a container image build, use the load mode described in Training without starting the application.
Warning
Never set micronaut.application.training.enabled or MICRONAUT_APPLICATION_TRAINING_ENABLED on a deployment target, whether in the configuration of the deployed application or in an environment variable shared by a platform or a cluster. The application would start, stop right away and exit with status 0, which most orchestrators read as a clean shutdown: the service restarts in a loop and nothing reports an error. The exit status has to stay 0 for the tools that write their output at exit, so the two warnings in the log are the only signal. Set the switch only in the build step that runs the training.
The switch acts on the EmbeddedApplication of the application, such as the HTTP server or a messaging application. An application without one is not stopped: Micronaut.run logs a warning that says so and returns as usual. The beans that require the switch are still created, so a startup listener written for the training run still runs. If such an application starts non-daemon threads of its own, the training run does not end until they do, so the application has to stop them itself.
To warm up the application before it stops, list the paths the HTTP server should send GET requests to itself, next to the switch. The requests are only sent in the start mode, which starts the server; the load mode starts no server and ignores these settings:
The warm-up runs once the server has started and listens on its port, after the other ServerStartupEvent listeners, for the server the application runs (not for other servers built with a server factory). It sends each path in order, repeat times, with java.net.HttpURLConnection, so no HTTP client is needed. repeat must be at least 1: a lower value fails the training run, even when no paths are set. The requests go to the configured micronaut.server.host, or to the loopback address when no host or a wildcard address such as 0.0.0.0 is set. The paths are sent as they are, so they include the context path if the server has one.
An I/O error or a response status of 500 or above fails the startup. A status from 400 to 499 does not: the warm-up logs a warning with the URL and the status, and the training run continues.
The warm-up runs while the server starts, so the Startup completed in time that a training run logs includes the warm-up requests. Do not compare it with the startup time of a normal run. The warm-up logs its own duration: Training warm-up sent 3 GET requests to http://127.0.0.1:8080 in 45ms.
For example, the following command trains a JDK AOT cache with a warm-up and exits once the cache is written:
The built-in warm-up only sends plain HTTP GET requests, without credentials:
If warm-up paths are set and the server has SSL enabled, the training run fails.
If the application rejects a request, for example with 401 or 403 because the path is secured, or with 429 from a rate limiter, the warm-up logs the warning described above and continues. A rejected request still exercises the server, the routing and the filters that rejected it, but not the code behind them. A path that does not exist (404) is reported the same way, so check the log of a training run for these warnings.
For other requests (a POST, authenticated requests, a server with SSL enabled, or a messaging application), write a startup listener that only exists during a training run:
@Singleton@Requires(property = "micronaut.application.training.enabled", pattern = "(?i)true")public class CustomWarmup implements ApplicationEventListener<ServerStartupEvent> { @Override public void onApplicationEvent(ServerStartupEvent event) { // send the requests to event.getSource(), throw to fail the training run }}
The pattern matches true in any case, as the switch does: with value = "true" a training run started with TRUE would stop the application without running the listener. For a messaging application, listen for ApplicationStartupEvent instead.
Warning
A training run in start mode executes application code, including the warm-up requests. If the application needs external services such as a database, run the training with an environment that configures them, or in load mode. Micronaut Test Resources cannot replace them: a training run disables it, as described in Test Resources.
Training without starting the application
Set micronaut.application.training.mode to load, next to the switch, for a training run that does not start the application. The environment variable is MICRONAUT_APPLICATION_TRAINING_MODE=load:
reads the configuration of the application as usual,
logs a warning that this JVM is a training run and does not serve traffic,
reads the bean definitions and evaluates their conditions without starting the application context, as configure() of ConfigurableBeanContext does,
loads every enabled bean definition and the classes it names: the bean class, the classes generated for AOP advice, and the types of its constructor, of its injected fields and methods and of its executable methods,
logs how many bean definitions it loaded, logs a second warning and closes the application context,
exits the JVM with status 0.
No bean is created and no bean class is initialized. No constructor, @PostConstruct method or startup event listener runs, no @Context or other eager bean is created, no method is scheduled, the EmbeddedApplication is not started and no port is bound. A bean that opens a connection when it is created therefore does not fail the run. A bean definition that cannot be loaded, for example because it names a class of an optional dependency that is absent, is skipped, and so is one with a condition that cannot be evaluated (see below). The run logs each skipped bean definition with the cause, and counts it in the line that ends the loading:
Training run (micronaut.application.training.mode=load): loaded 359 of 411 bean definitions and the 655 types they name in 43ms, skipped 0 that could not be evaluated or loaded
The mode is read only when micronaut.application.training.enabled is true. Its values are start, the default, and load, in any case; any other value fails the training run. The load mode works for every application, including one without an EmbeddedApplication, which a start run cannot stop. In the test environment System.exit is not called and Micronaut.run returns the application context, closed and never started.
The warm-up does not apply to the load mode, because no server is started. If micronaut.application.training.warmup settings are present, a warning says that they are ignored.
What still runs in load mode is the code that runs whenever an application context is configured:
the property source loaders. The one of Micronaut Test Resources supplies nothing, because a training run disables it (see Test Resources). If the application reads distributed configuration at startup through the bootstrap context, it still does, and the beans of the bootstrap context that read it are created.
the conditions of the bean definitions, custom Condition classes included. A condition cannot look up a bean: the application context is not running, so the lookup throws an IllegalStateException and no bean is created. The bean definition with that condition is skipped and logged with that cause, although a normal start evaluates the condition.
Choosing a mode
The tools that a training run feeds record what the JVM loads and what it executes. A load run loads the classes of the beans and executes the framework code that builds the application context, but none of the code that creates the beans, starts the server and handles requests. A JDK AOT cache or a CDS archive trained in load mode therefore holds fewer classes, and none of the linked lambdas and method profiles of that code: the application starts faster than without a cache, but slower than with a cache trained in start mode.
Training run
When to use it
start with warm-up paths
The application can start where the training runs: the services it needs are available, or a training environment replaces them. This gives the most complete cache.
start without warm-up paths
As above, when the application has no request that is safe to send.
load
The application cannot start where the training runs. The cache is smaller and keeps only part of the gain.
For a GraalVM profile for profile-guided optimization or for the native-image-agent, use the start mode: both record what the application executes, and a load run executes almost none of it.
Test Resources
Micronaut Test Resources supplies the properties that the configuration of an application lacks, such as datasources.default.url, and starts a container for each one that is read. Left enabled in a training run, it would change what the run trains: a load run would ask the Test Resources server which properties it can supply while it reads the configuration, and a condition that compares the value of such a property would make the server start a container; a start run would train against the containers. A training run therefore disables Test Resources, in both modes:
Before it reads the configuration, the training run sets the system property micronaut.test.resources.enabled to false, the switch of the Test Resources client. The client then contacts no server, supplies no property and starts no container. The system property gets its previous value back once the configuration has been read; the configuration of the training run keeps false.
This needs the training run switch before the configuration is read: set micronaut.application.training.enabled as a system property, as the MICRONAUT_APPLICATION_TRAINING_ENABLED environment variable or as an argument of the application. If Test Resources is on the class path and the switch is only set in the configuration of the application, for example in a configuration file, the training run fails with a message that says so.
A Test Resources module that does not read that switch, such as micronaut-test-resources-embedded, cannot be disabled: if one is on the class path, the training run fails.
Test Resources reuses a client that the same JVM has already created from system properties, whatever the switch says, and the training run cannot tell. In practice this is a test that starts an application context with Test Resources and then runs a training run in the same JVM: run such a training run in a JVM of its own.
The Micronaut Gradle plugin adds the Test Resources client to the developmentOnly and testRuntimeOnly configurations, and a Maven project generated by Micronaut Launch declares it with the provided scope. Neither is part of the runtime class path that a JAR or a container image is built from, so a training run in an image build only sees Test Resources if the build adds it to that class path.
Netty beans are enabled by default. Set netty.enabled=false to disable all Netty integration, or micronaut.server.netty.enabled=false to disable only the Netty server. These properties back RequiresNetty and RequiresNettyServer.
The following example shows how to tweak configuration options for the server via your configuration file (e.g application.yml):
http10-keep-alive keeps the connection of an HTTP/1.0 client that sends Connection: keep-alive after a response of known length, and answers with Connection: keep-alive. A response has a known length when it has a Content-Length or has no body, e.g. a 204 or 304 response or the response to a HEAD request. A response of unknown length still ends with the connection. By default, the connection of an HTTP/1.0 client ends after each response.
reject-unsupported-http-versions answers a request of an HTTP major version other than 1, e.g. HTTP/9.9 or HTTP/2.0 on an HTTP/1 connection, which the request decoder accepts, with 505 HTTP Version Not Supported over HTTP/1.1 and closes the connection. A later minor version of HTTP/1, e.g. HTTP/1.2, is served as HTTP/1.1 (RFC 9112 section 2.3). By default, such a request is served and answered with its own version.
Using Native Transports
The native Netty transports add features specific to a particular platform, generate less garbage, and generally improve performance when compared to the NIO-based transport.
To enable native transports, first add a dependency:
The first available transport out of those listed will be used. If you want to make sure native transports will be used in all environments, remove the nio entry. An exception will be thrown if no native transport is available.
Note
Netty enables simplistic sampling resource leak detection which reports there is a leak or not, at the cost of small overhead. You can enable it by setting property netty.resource-leak-detector-level to one of: DISABLED (default), SIMPLE, PARANOID or ADVANCED.
40.1 Configuring Server Thread Pools
The HTTP server is built on Netty which is designed as a non-blocking I/O toolkit in an event loop model.
The Netty worker event loop uses the "default" named event loop group. This can be configured through micronaut.netty.event-loops.default.
Important
The event loop configuration under micronaut.server.netty.worker is only used if the event-loop-group is set to a name which doesn’t correspond to any micronaut.netty.event-loops configuration. This behavior is deprecated and will be removed in a future version. Use micronaut.netty.event-loops.* for any event loop group configuration beyond setting the name through event-loop-group. This does not apply to the parent event loop configuration (micronaut.server.netty.parent).
Tip
The parent event loop can be configured with micronaut.server.netty.parent with the same configuration options.
The server can also be configured to use a different named worker event loop:
The default value for the number of threads is the value of the system property io.netty.eventLoopThreads, or if not specified, the available processors x 2.
See the following table for configuring event loops:
40.1.1 Blocking Operations
When dealing with blocking operations, the Micronaut framework shifts the blocking operations to an unbound, caching I/O thread pool by default. You can configure the I/O thread pool using the ExecutorConfiguration named io. For example:
The above configuration creates a fixed thread pool with 75 threads.
40.1.2 Virtual Threads
Since Java 19, the JVM includes experimental support for virtual threads ("project loom"). As it is a preview feature, you need to pass --enable-preview as a JVM parameter to enable it.
The Micronaut framework will detect virtual thread support and use it for the executor named blocking if available. If virtual threads are not supported, this executor will be aliased to the io thread pool.
To use the blocking executor, simply mark e.g. a controller with ExecuteOn:
Micronaut HTTP Server Netty 4.9 introduces an experimental mode to run virtual threads on the Netty event loop. This
can improve performance of virtual threads, since it avoids a context switch between the Netty event loop and the JDK
ForkJoinPool.
This mode requires access to internal JDK APIs. Please run your application with
--add-opens=java.base/java.lang=ALL-UNNAMED. Then, enable the loom-carrier flag for the event loop, e.g. micronaut.netty.event-loops.loom-carrier=true
You can use the @Blocking annotation to mark methods as blocking.
If you set micronaut.server.thread-selection to AUTO, the Micronaut framework offloads the execution of methods annotated with @Blocking to the IO thread pool (See: TaskExecutors).
Note
@Blocking only works if you are using AUTO thread selection. Micronaut framework defaults to MANUAL thread selection since Micronaut 2.0. We recommend the usage of @ExecuteOn annotation to execute the blocking operations on a different thread pool. @ExecutesOn works for both MANUAL and AUTO thread selection.
There are some places where the Micronaut framework uses @Blocking internally:
Resolves either remote or local PropertySource instances for the current Environment.
Tip
Micronaut Data also utilizes @Blocking internally for some transaction operations, CRUD interceptors, and repositories.
40.2 Configuring the Netty Client Pipeline
You can customize the Netty client pipeline by writing a Bean Event Listener that listens for the creation of a Registry.
The ChannelPipelineCustomizer interface defines constants for the names of the various handlers that the Micronaut framework registers.
As an example the following code sample demonstrates registering the Logbook library which includes additional Netty handlers to perform request and response logging:
Customizing the Netty server pipeline for Logbook
40.3 Configuring the Netty Server Pipeline
You can customize the Netty server pipeline by writing a Bean Event Listener that listens for the creation of Registry.
The ChannelPipelineCustomizer interface defines constants for the names of the various handlers the Micronaut framework registers.
As an example the following code sample demonstrates registering the Logbook library which includes additional Netty handlers to perform request and response logging:
Customizing the Netty server pipeline for Logbook
Warning
As of version 4.5.0, the Micronaut netty HTTP server does not use per-request channels for HTTP/2 anymore. This makes many pipeline modifications (including logbook) difficult or impossible to implement. To revert to the old behavior, use the micronaut.server.netty.legacy-multiplex-handlers=true property.
40.4 Netty Transport and Codec Extensions
Named event-loop transports are selected by the transport list on an event-loop configuration. The first registered, available factory in that list is selected. An external factory does not become the default merely because its bean is present. A list containing only an unavailable transport fails at startup.
Each listener can borrow a named worker group through event-loop-group. TCP and Unix listeners can also borrow a named acceptor group through parent-event-loop-group. When omitted, these properties inherit the server-wide groups. The selected transport supplies the listener’s channels; the acceptor and worker groups must support those channels. QUIC uses its selected worker group and the existing QUIC implementation.
For example, TCP and QUIC can use separate NIO groups:
The QUIC listener also needs the existing server certificate configuration. Named groups remain owned by their registry: stopping a listener or stopping only the server does not shut down borrowed groups. Changes to listener group selection require a server restart.
The experimental NettyZlibCodecFactory SPI replaces construction of HTTP compression handlers independently of the selected transport. Supply one bean implementing this interface to replace the stock Netty factory. The factory receives the existing compression level, window size and memory level for encoding, and the wrapper mode and output allocation limit for decoding. It must return a fresh stateful handler for every call (or a sharable WebSocket extension handler) and release its resources through the handler lifecycle.
The selected factory is used for server response compression, request decompression, and managed client response decompression over HTTP/1, HTTP/2 and HTTP/3. Existing compression negotiation, thresholds, body limits and framing remain in Core. Default methods also support Brotli, Zstandard, and framed Snappy using stock Netty codecs. Override their availability and construction methods to supply an alternative implementation. A provider can advertise Brotli or Zstandard without initializing the optional stock JNI libraries. Brotli encoding uses quality 4 and text mode; Zstandard receives the configured compression level and maximum encode size and uses the Netty default block size. Framed Snappy retains Netty’s default checksum policy. Existing providers implementing only gzip and deflate continue to use the stock defaults for these formats. A client built directly outside an application context uses stock codecs; set zlibCodecFactory on DefaultHttpClient.builder() to opt in explicitly.
TLS providers can extend NettySslContextBuilder through the existing server and client SSL factories. Protected accessors expose key and trust material, protocols, cipher policy, ALPN and client authentication. Legacy server and client SSL builders also expose a protected createSslContext hook. Its default keeps existing material loading, builder customization and exception translation. An override must preserve configured TLS policy and required customizations, including ACME ALPN challenges. QUIC TLS retains its separate construction path.
The factory also creates server and managed client WebSocket compression extension handlers. These methods receive the endpoint’s @OnMessage(maxPayloadLength) value, allowing providers to bound decompressed messages. Their defaults preserve Netty’s existing extension negotiation, compression settings, allocation policy, and sharable client handler. Client compression must be enabled to invoke its provider hook; server handlers are created only for actual upgrades. Netty continues to handle WebSocket framing, masking, and UTF-8 validation.
40.5 Advanced Listener Configuration
Instead of configuring a single port, you can also specify each listener manually.
unixListener is a listener name, and can be an arbitrary value
Note
To use an abstract domain socket instead of a normal one, prefix the path with a NUL character, like "\0/run/micronaut.socket"
systemd socket activation support
The HTTP server can be configured to use an existing file descriptor. With this feature, you can use socket created by systemd and passed to the micronaut process:
micronaut: netty: event-loops: # use epoll transport parent: transport: epoll default: transport: epoll listeners: systemd: fd: 3 # systemd passes the server socket as fd 3 bind: false # do not bind again, systemd already did this
[Unit]Description=Micronaut HTTP server with socket activationAfter=network.target app.socketRequires=app.socket[Service]Type=simpleExecStart=/usr/bin/java -jar /app.jar
Now your Micronaut application can be started by a client connecting to http://127.0.0.1:8080.
Note
The nio transport only supports fd: 0 ("inetd style"), through the JDK System.inheritedChannel() API. If you
wish to use the default fd: 3 recommended by systemd, you must use the epoll transport.
inetd-style socket activation
systemd also supports inetd-style socket activation. In that mode, instead of passing a listening socket fd
(ServerSocketChannel), systemd will pass an already accepted fd (SocketChannel) representing an individual TCP
connection. systemd will start a new instance of the service for each new TCP connection.
The Micronaut HTTP server supports this mode through the accepted-fd config option. An accepted-fd will be
registered with the listener it is declared on, and essentially treated like a connection that was accepted by the
listener. The netty parent channel will be set to the listener server socket, which is important to avoid a
netty bug in connection with HTTP/2.
Warning
Epoll does not support setting the parent channel, so HTTP/2 may not work with accepted-fd and epoll.
micronaut: netty: listeners: systemd: accepted-fd: 0 # inetd-style socket activation typically uses fd 0 (stdin) bind: false # do not bind, we only use the server channel as the parent
By setting server-socket: false, you can disable the parent channel entirely. This may save some slight startup time,
but may cause problems with HTTP/2.
Warning
Because inetd-style socket activation typically uses fd 0, it hijacks stdin and stdout. You must take care to
never emit any output on stdout, as this may corrupt the HTTP response, or even show up for the user. In particular,
you must configure logback to log to System.err instead, and disable the Micronaut startup banner.
Micronaut CORS configuration applies by default to all endpoints in the running application.
As an alternative, CORS configuration can be applied in a more fine-grained manner to specific routes using the
@CrossOrigin annotation. This is applied to @Controller to apply the CORS configuration to all endpoints in the controller. Alternatively, the annotation can be applied to specific endpoints on a controller, for even more fine-grained control.
The @CrossOrigin annotation maps with a one-to-one correspondence to application-wide CorsOriginConfiguration configuration properties. To specify just an allowed origin, use @CrossOrigin("https://foo.com"). To specify additional configuration details, use a combination of annotation attributes the same as you would for specifying global CorsOriginConfiguration properties.
The following example demonstrates how the annotation might be applied to a specific endpoint. To enable CORS for all endpoints in the controller, move the annotation to the class level and configure it appropriately.
@CrossOrigin Example
Note
The @CrossOrigin annotation uses the same defaults as the application configuration alternative, when corresponding annotation attributes are not set. See CORS configuration for details.
40.8.2 CORS via Configuration
To enable processing of CORS requests, modify your configuration in the application configuration file:
By only enabling CORS processing, a "wide open" strategy is adopted that allows requests from any origin.
To change the settings for all origins or a specific origin, change the configuration to provide one or more "configurations". By providing any configuration, the default "wide open" configuration is not configured.
In the above example, three configurations are provided. Their names (all, web, mobile) are not important and have no significance inside Micronaut. They are there purely to be able to easily recognize the intended user of the configuration.
The same configuration properties can be applied to each configuration. See CorsOriginConfiguration for properties that can be defined. The values of each configuration supplied will default to the default values of the corresponding fields.
When a CORS request is made, configurations are searched for allowed origins that match exactly or match the request origin through a regular expression.
40.8.3 Allowed Origins
Don’t define allowed-origins or allowed-origins-regex to allow any origin for a given configuration.
For multiple valid origins, set the allowed-origins key of the configuration to a list of strings.
You can also define via allowed-origins-regex a regular expression (^http(|s)://www\.google\.com$). The Regular expression is passed to Pattern#compile and compared to the request origin with Matcher#matches.
Use the allowed-origins-regex configuration judiciously. You may accidentally make an insecure configuration which could be targeted by an attacker registering domains targeting the regular expression.
Note
allowed-origins-regex and allowed-origins can be combined. However, if using the former and the latter is not set explicitly then allowed-origins defaults to none rather than any to avoid unexpected allowed origins.
40.8.4 Allowed Methods
To allow any request method for a given configuration, don’t include the allowed-methods key in your configuration.
For multiple allowed methods, set the allowed-methods key of the configuration to a list of strings.
To configure the headers that are sent in the response to a CORS request through the Access-Control-Expose-Headers header, include a list of strings for the exposed-headers key in your configuration. None are exposed by default.
Access from private network is allowed by default for CORS requests. To disallow acces from local network, set the allow-private-network option to false.
By default, when a header has multiple values, multiple headers are sent, each with a single value. It is possible to change the behavior to send a single header with a comma-separated list of values by setting a configuration option.
micronaut.server.cors.single-header=true
micronaut: server: cors: single-header: true
[micronaut.server.cors]single-header = true
micronaut { server { cors { singleHeader = true } }}
The Micronaut framework supports HTTPS out of the box. By default, HTTPS is disabled and all requests are served using HTTP. To enable HTTPS support, define TLS material using named certificate providers and reference them from your SSL configuration. See Certificate providers for details.
HTTPS Configuration Example (self-signed for development)
This configuration will generate a warning in the browser.
Using a valid X.509 certificate (PEM)
It is also possible to configure a Micronaut application to use an existing valid x509 certificate, for example one created with Let’s Encrypt. You will need the server.crt and server.key files.
With this configuration, if we start a Micronaut application and connect to https://localhost:8443 we still see the warning in the browser, but if we inspect the certificate we can check that it is the one generated by Let’s Encrypt.
Finally, we can test that the certificate is valid for the browser by adding an alias to the domain in /etc/hosts file:
The more traditional approach to managing certificates in Java is with a key store, either in PKCS#12 or the older JKS format. You can convert your PEM files to PKCS#12 as follows:
Reference the PKCS#12 in a provider and point SSL to it:
Start Micronaut, and the application will run on https://localhost:8443 using the certificate in the keystore.
Refreshing/Reloading HTTPS Certificates
Keeping HTTPS certificates up-to-date after expiry can be a challenge. A great solution to this is Automated Certificate Management Environment (ACME) and the Micronaut ACME Module which provides support for automatically refreshing certificates from a certificate authority.
If the use of a certificate authority is not possible, and you need to manually update certificates from disk then you should use the file-based certificate provider. As long as the underlying file system supports file watching, the provider will automatically recognize an updated key or certificate file.
Serving with a Provided SSLContext
When the key and trust material are managed elsewhere, the server can serve with a ready javax.net.ssl.SSLContext bean instead. Name the bean with context-name; the client authentication mode, protocols and ciphers still apply, and ALPN is negotiated for HTTP/2:
A context set programmatically with ServerSslConfiguration.setSslContext(..) takes precedence over context-name. A provided context cannot be used for HTTP/3.
40.10 Enabling HTTP and HTTPS
The Micronaut framework supports binding both HTTP and HTTPS. To enable dual protocol support, modify your configuration. For example:
You must configure SSL for HTTPS to work. In this example we are just using a self-signed certificate with build-self-signed, but see Securing the Server with HTTPS for other configurations
dual-protocol enables both HTTP and HTTPS is an opt-in feature - setting the dualProtocol flag enables it. By default, the Micronaut framework only enables one.
It is also possible to redirect automatically all HTTP request to HTTPS. Besides the previous configuration, you need to enable this option. For example:
optionally specify a logger-name, which defaults to HTTP_ACCESS_LOGGER
optionally specify a log-format, which defaults to the Common Log Format
Logback Configuration
In addition to enabling the access logger, you must add a logger for the specified or default logger name. For instance using the default logger name for logback:
%b - Bytes sent, excluding HTTP headers, or '-' if no bytes were sent
%B - Bytes sent, excluding HTTP headers
%h - Remote host. No reverse DNS lookup is done, so this is the remote IP address
%H - Request protocol
%{<header>}i - Request header. If the argument is omitted (%i) all headers are printed
%{<header>}o - Response header. If the argument is omitted (%o) all headers are printed
%{<cookie>}C - Request cookie (COOKIE). If the argument is omitted (%C) all cookies are printed
%{<cookie>}c - Response cookie (SET_COOKIE). If the argument is omitted (%c) all cookies are printed
%l - Remote logical username from identd (always returns '-')
%m - Request method
%p - Local port
%q - Query string (excluding the '?' character)
%r - First line of the request
%{<attribute>}r - Value of a Micronaut request attribute (see HttpRequest.getAttribute), read when the response headers are written, or '-' if the attribute is not set. Control characters in the value are escaped. For example, a gateway that stores its route id in the routeId request attribute can log it with %{routeId}r
%s - HTTP status code of the response
%{<format>}t - Date and time. If the argument is omitted, Common Log Format is used ("'['dd/MMM/yyyy:HH:mm:ss Z']'").
If the format starts with begin: (default) the time is taken at the beginning of the request processing. If it starts with end: it is the time when the log entry gets written, close to the end of the request processing.
The format should follow DateTimeFormatter syntax.
%{property}u - Remote authenticated user. When micronaut-session is on the classpath, returns the session id if the argument is omitted, or the specified property otherwise prints '-'
%U - Requested URI
%v - Local server name
%D - Time taken to process the request, in milliseconds
%T - Time taken to process the request, in seconds
In addition, you can use the following aliases for common patterns:
combined - %h %l %u %t "%r" %s %b "%{Referer}i" "%{User-Agent}i" for Combined Log Format
40.12 Starting Secondary Servers
The Micronaut framework supports the programmatic creation of additional Netty servers through the NettyEmbeddedServerFactory interface.
This is useful in cases where you, for example, need to expose distinct servers over different ports with potentially differing configurations (HTTPS, thread resources etc).
The following example demonstrates how to define a Factory Bean that starts an additional server using a programmatically created configuration:
Programmatically creating Secondary servers
With this class in place when the ApplicationContext starts the server will also be started with the appropriate configuration.
Thanks to the definition of the ServiceInstanceList in step 8, you can then inject a client into your tests to test the secondary server:
Injecting the server or client
41 Server Side View Rendering
The Micronaut framework supports Server Side View Rendering.
See the documentation for Micronaut Views for more information.
GraphQL is a query language for building APIs that provides an intuitive and flexible syntax and a system for describing data requirements and interactions.
See the documentation for Micronaut GraphQL for more information on how to build GraphQL applications with Micronaut.
44 The HTTP Client
Client communication between Microservices is a critical component of any Microservice architecture. With that in mind, Micronaut framework includes an HTTP client that has both a low-level API and a higher-level AOP-driven API.
Tip
Regardless whether you choose to use Micronaut’s HTTP server, you may wish to use the Micronaut HTTP client in your application since it is a feature-rich client implementation.
45 HTTP Client Implementations
There are several implementations of the Micronaut HTTP Client.
45.1 HTTP Client based on Netty
To use an implementation based on Netty, add the following dependency to your build:
The same option applies to a client created with a DefaultHttpClientConfiguration, together with SslConfiguration.setSslContext(..) to connect with a ready javax.net.ssl.SSLContext instead of the configured key and trust stores. Without endpoint identification, a javax.net.ssl.HostnameVerifier set with AbstractClientSslConfiguration.setHostnameVerifier(..) can still check the host name after the handshake:
A client with a hostname verifier
The client calls the verifier with the host name and the SSLSession once the handshake completes, for HTTP/1.1 and HTTP/2 (including protocol negotiation with ALPN) and for WebSocket connections. When it returns false or throws, the connection fails with a javax.net.ssl.SSLPeerUnverifiedException. The verifier runs in addition to endpoint identification, so to accept a host the default check rejects, disable endpoint identification as well. A hostname verifier is not supported for HTTP/3.
Warning
Disabling host name verification leaves your application potentially vulnerable to Man-in-the-Middle attacks and should only be done with the utmost care typically only in a development / testing environment. Never disable host name verification in production or in a scenario where you are making remote calls over the internet.
45.2 HTTP Client based on the Java HTTP Client
To use an implementation based on Java HTTP Client, add the following dependency to your build:
Since the higher level API is built on the low-level HTTP client, we first introduce the low-level client.
47 Using the Low-Level HTTP Client
The HttpClient interface forms the basis for the low-level API. This interfaces declares methods to help ease executing HTTP requests and receiving responses.
The majority of the methods in the HttpClient interface return Reactive Streams Publisher instances, which is not always the most useful interface to work against.
Micronaut’s Reactor HTTP Client dependency ships with a sub-interface named ReactorHttpClient. It provides a variation of the HttpClient interface that returns Project ReactorFlux types.
The AsyncHttpClient exposes CompletionStage results for full responses and their converted bodies. Micronaut’s clients treat the future returned by exchange or retrieve as owning the request. Before that future completes, cancelling it or completing it exceptionally, including with orTimeout, aborts outstanding work. Cancellation is a best-effort hint: it cannot undo an action the remote server already performed.
A dependent stage created with thenApply, thenCompose, or another standard stage method does not own the request. Cancelling or timing out only that stage leaves the original request running. Keep the original future and apply a deadline to it when the deadline should abort the request. Do not block on these futures on a client event loop.
This ownership rule applies to client operations, not shared discovery or load-balancer SPI stages. A caller must not cancel or externally complete a shared SPI stage, since other callers may depend on it. Apply a caller-specific timeout to a dependent stage instead.
47.1 Sending your first HTTP request
Obtaining a HttpClient
There are a few ways to obtain a reference to an HttpClient. The most common is to use the Client annotation. For example:
The above example injects a client that targets the Twitter API.
@field:Client("\${myapp.api.twitter.url}") @Inject lateinit var httpClient: HttpClient
The above Kotlin example injects a client that targets the Twitter API using a configuration path. Note the required escaping (backslash) on "\${path.to.config}" which is necessary due to Kotlin string interpolation.
The Client annotation is also a custom scope that manages the creation of HttpClient instances and ensures they are stopped when the application shuts down.
The value you pass to the Client annotation can be one of the following:
A relative URI, in which case the targeted server will be the current server (useful for testing)
A service identifier. See the section on Service Discovery for more information on this topic.
Another way to create an HttpClient is with the static create method of HttpClient, however this approach is not recommended as you must ensure you manually shutdown the client, and of course no dependency injection will occur for the created client.
Performing an HTTP GET
Generally there are two methods of interest when working with the HttpClient. The first is retrieve, which executes an HTTP request and returns the body in whichever type you request (by default a String) as Publisher.
The retrieve method accepts an HttpRequest or a String URI to the endpoint you wish to request.
The following example shows how to use retrieve to execute an HTTP GET and receive the response body as a String:
Using retrieve
String uri = UriBuilder.of("/hello/{name}") .expand(Collections.singletonMap("name", "John")) .toString();assertEquals("/hello/John", uri);String result = client.toBlocking().retrieve(uri);assertEquals("Hello John", result);
uri = str(UriBuilder.of("/hello/{name}").expand({"name": "John"}))assert "/hello/John" == uriresult = self.client.toBlocking().retrieve(uri)assert "Hello John" == result
val uri = UriBuilder.of("/hello/{name}") .expand(Collections.singletonMap("name", "John")) .toString()uri shouldBe "/hello/John"val result = client.toBlocking().retrieve(uri)result shouldBe "Hello John"
when:String uri = UriBuilder.of("/hello/{name}") .expand(name: "John")then:"/hello/John" == uriwhen:String result = client.toBlocking().retrieve(uri)then:"Hello John" == result
Note that in this example, for illustration purposes we call toBlocking() to return a blocking version of the client. However, in production code you should not do this and instead rely on the non-blocking nature of the Micronaut HTTP server.
For example the following @Controller method calls another endpoint in a non-blocking manner:
Using the HTTP client without blocking
Tip
Using Reactor (or RxJava if you prefer) you can easily and efficiently compose multiple HTTP client calls without blocking (which limits the throughput and scalability of your application).
Debugging / Tracing the HTTP Client
To debug requests being sent and received from the HTTP client you can enable tracing logging via your logback.xml file:
To enable client-specific logging you can configure the default logger for all HTTP clients. You can also configure different loggers for different clients using Client-Specific Configuration. For example, in your configuration file (e.g application.yml):
The previous example demonstrates using the static methods of the HttpRequest interface to construct a MutableHttpRequest instance. Like the name suggests, a MutableHttpRequest can be mutated, including the ability to add headers, customize the request body, etc. For example:
The above example adds a header (X-My-Header) to the response before it is sent. The MutableHttpRequest interface has more convenience methods that make it easy to modify the request in common ways.
Reading JSON Responses
Microservices typically use a message encoding format such as JSON. Micronaut’s HTTP client leverages Jackson for JSON parsing, hence any type Jackson can decode can be passed as a second argument to retrieve.
For example consider the following @Controller method that returns a JSON response:
Returning JSON from a controller
@Get("/greet/{name}")Message greet(String name) { return new Message("Hello " + name);}
public class Message { private final String text; @JsonCreator public Message(@JsonProperty("text") String text) { this.text = text; } public String getText() { return text; }}
imports
from micronaut.core.annotation import Introspected, ReflectiveAccess
The above example decodes the response into a Map representing the JSON. You can use the Argument.of(..) method to customize the type of the key and value:
Decoding the response body to a Map
Whilst retrieving JSON as a map can be desirable, typically you want to decode objects into POJOs. To do that, pass the type instead:
Note how you can use the same Java type on both the client and the server. The implication of this is that typically you define a common API project where you define the interfaces and types that define your API.
Decoding Other Content Types
If the server you communicate with uses a custom content type that is not JSON, by default Micronaut’s HTTP client will not know how to decode this type.
To resolve this, register MediaTypeCodec as a bean, and it will be automatically picked up and used to decode (or encode) messages.
Receiving the Full HTTP Response
Sometimes receiving just the body of the response is not enough, and you need other information from the response such as headers, cookies, etc. In this case, instead of retrieve use the exchange method:
Receiving the Full HTTP Response
The above example receives the full HttpResponse from which you can obtain headers and other useful information.
47.2 Posting a Request Body
All the examples so far have used the same HTTP method i.e GET. The HttpRequest interface has factory methods for all the different HTTP methods. The following table summarizes them:
A create method also exists to construct a request for any HttpMethod type. Since the POST, PUT and PATCH methods require a body, a second argument which is the body object is required.
The following example demonstrates how to send a simple String body:
Sending a String body
Sending JSON
The previous example sends plain text. To send JSON, pass the object to encode to JSON (whether that be a Map or a POJO) as long as Jackson is able to encode it.
For example, you can create a Message from the previous section and pass it to the POST method:
Sending a JSON body
With the above example the following JSON is sent as the body of the request:
If include some properties of the object in the URI, you can use a URI template.
For example imagine you have a Book class with a title property. You can include the title in the URI template and then populate it from an instance of Book. For example:
Sending a JSON body with a URI template
Flux<HttpResponse<Book>> call = Flux.from(client.exchange( POST("/amazon/book/{title}", new Book("The Stand")), Book.class));
val call = client.exchange( POST("/amazon/book/{title}", Book("The Stand")) .contentType(MediaType.APPLICATION_FORM_URLENCODED), Book::class.java)
Flux<HttpResponse<Book>> call = client.exchange( POST("/amazon/book/{title}", new Book("The Stand")) .contentType(MediaType.APPLICATION_FORM_URLENCODED), Book)
Note that Jackson can bind form data too, so to customize the binding process use Jackson annotations.
47.3 Multipart Client Uploads
The Micronaut HTTP Client supports multipart requests. To build a multipart request, set the content type to multipart/form-data and set the body to an instance of MultipartBody.
For example:
Creating the body
Creating a request
47.4 Streaming JSON over HTTP
Micronaut’s HTTP client includes support for streaming data over HTTP via the ReactorStreamingHttpClient interface which includes methods specific to streaming including:
To use JSON streaming, declare a controller method on the server that returns a application/x-json-stream of JSON objects. For example:
Streaming JSON on the Server
Note
The server does not have to be written in Micronaut, any server that supports JSON streaming will do.
Then on the client, subscribe to the stream using jsonStream and every time the server emits a JSON object the client will decode and consume it:
Streaming JSON on the Client
Note neither the server nor the client in the example above perform any blocking I/O.
47.5 Configuring HTTP clients
Global Configuration for All Clients
Netty beans are enabled by default. Set netty.enabled=false to disable all Netty integration, or micronaut.http.client.netty.enabled=false to disable only the Netty client. These properties back RequiresNetty and RequiresNettyClient.
The default HTTP client configuration is a Configuration Properties named DefaultHttpClientConfiguration that allows configuring the default behaviour for all HTTP clients. For example, in your configuration file (e.g application.yml):
To have separate configuration per-client, there are a couple of options. You can configure Service Discovery manually in your configuration file (e.g. application.yml) and apply per-client configuration:
For the Netty HTTP client, you can also plug in a custom DNS resolver by referencing a named Netty AddressResolverGroup bean with the address-resolver-group-name property. This is useful when you need custom name resolution behavior for a specific client.
Define a named AddressResolverGroup bean
In the test suites, the named client is configured programmatically with the same properties:
Programmatic client configuration example
The configured name must match an existing bean. When address-resolver-group-name is set, dns-resolution-mode is ignored.
WARN: This client configuration can be used in conjunction with the @Client annotation, either by injecting an HttpClient directly or use on a client interface. In any case, all other attributes on the annotation will be ignored except the service id.
Connections using normal HTTP (without TLS/SSL) use HTTP/1.1. This can be configured using the plaintext-mode configuration option.
Secure connections (i.e. HTTPS, with TLS/SSL) use a feature called "Application Layer Protocol Negotiation" (ALPN) that is part of TLS to select the HTTP version. If the server supports HTTP/2, the Micronaut HTTP Client will use that capability by default, but if it doesn’t, HTTP/1.1 is still supported. This is configured using the alpn-modes option, which is a list of supported ALPN protocol IDs ("h2" and "http/1.1").
Note
The HTTP/2 standard forbids the use of certain less secure TLS cipher suites for HTTP/2 connections. When the HTTP client supports HTTP/2 (which is the default), it will not support those cipher suites. Removing "h2" from alpn-modes will enable support for all cipher suites.
Each HTTP/1.1 connection can only support one request at a time, but can be reused for subsequent requests using the keep-alive mechanism. HTTP/2 connections can support any number of concurrent requests.
To remove the overhead of opening a new connection for each request, the Micronaut HTTP Client will reuse HTTP connections wherever possible. They are managed in a connection pool. HTTP/1.1 connections are kept around using keep-alive and are used for new requests, and for HTTP/2, a single connection is used for all requests.
By setting the pool.enabled property to false, you can disable connection reuse. The pool is still used and other configuration options (e.g. concurrent HTTP 1 connections) still apply, but one connection will only serve one request.
Configuring Event Loop Groups
By default, the Micronaut framework shares a common Netty EventLoopGroup for worker threads and all HTTP client threads.
This EventLoopGroup can be configured via the micronaut.netty.event-loops.default property:
You can also use the micronaut.netty.event-loops setting to configure one or more additional event loops. The following table summarizes the properties:
For example, if your interactions with an HTTP client involve CPU-intensive work, it may be worthwhile configuring a separate EventLoopGroup for one or all clients.
The following example configures an additional event loop group called "other" with 10 threads:
If an HTTP response is returned with a code of 400 or higher, an HttpClientResponseException is created. The exception contains the original response. How that exception gets thrown depends on the return type of the method.
For blocking clients, the exception is thrown and should be caught and handled by the caller. For reactive clients, the exception is passed through the publisher as an error.
47.7 Bind Errors
Often you want to consume an endpoint and bind to a POJO if the request is successful and bind to a different POJO if an error occurs. The following example shows how to invoke exchange with a success and error type.
@Controller("/books")public class BooksController { @Get("/{isbn}") public HttpResponse find(String isbn) { if (isbn.equals("1680502395")) { Map<String, Object> m = new HashMap<>(); m.put("status", 401); m.put("error", "Unauthorized"); m.put("message", "No message available"); m.put("path", "/books/" + isbn); return HttpResponse.status(HttpStatus.UNAUTHORIZED).body(m); } return HttpResponse.ok(new Book("1491950358", "Building Microservices")); }}
@Controller("/books")class BooksController: @Get("/{isbn}") def find(self, isbn: str) -> HttpResponse: if isbn == "1680502395": body = { "status": 401, "error": "Unauthorized", "message": "No message available", "path": "/books/" + isbn, } return HttpResponse.status(HttpStatus.UNAUTHORIZED).body(body) return HttpResponse.ok(Book("1491950358", "Building Microservices"))
@Controller("/books")class BooksController { @Get("/{isbn}") fun find(isbn: String): HttpResponse<*> { if (isbn == "1680502395") { val m = mapOf( "status" to 401, "error" to "Unauthorized", "message" to "No message available", "path" to "/books/$isbn" ) return HttpResponse.status<Any>(HttpStatus.UNAUTHORIZED).body(m) } return HttpResponse.ok(Book("1491950358", "Building Microservices")) }}
A common requirement in Microservice environments is to proxy requests in a Gateway Microservice to other backend Microservices.
The regular HttpClient API is designed around simplifying message exchange and is not designed for proxying requests. For this case, use the ProxyHttpClient, which can be used from an HTTP Server Filter to proxy requests to backend Microservices.
The following example demonstrates rewriting requests under the URI /proxy to the URI /real onto the same server (although in a real environment you generally proxy to another server):
Proxy filter that rewrites original requests
Note
The ProxyHttpClient API is a low-level API that can be used to build a higher-level abstraction such as an API Gateway.
Forwarding Headers
To tell the backend who the original client is, write the forwarding headers of the proxied request with ForwardedHeaders: X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host, X-Forwarded-Port, X-Forwarded-Prefix and the RFC 7239 Forwarded header.
A trusted proxy is trusted with the values, not with their syntax: an X-Forwarded-Proto with a scheme that is not http, https, ws or wss, or an X-Forwarded-Port with a port that is not a number between 1 and 65535, is dropped, and the value of this hop is sent instead. Such a port is also dropped from an X-Forwarded-Host, and such a scheme or port from the proto and host of a Forwarded element. A list of valid values, one per proxy, is kept.
49 Raw Exchanges with RawHttpClient
The RawHttpClient and its asynchronous form AsyncRawHttpClient exchange requests and responses as raw bytes: the request body is a CloseableByteBody that the client sends as it is, and the response body is handed back unread. A gateway uses them to relay requests to backends.
Each exchange can take RawRequestOptions, starting from RawRequestOptions.getDefault() or RawRequestOptions.proxy(). Two of them help a gateway:
readIdleTimeout replaces the read timeout of the client for one exchange: the exchange fails when no response bytes arrive for that long. It can be longer as well as shorter than the read-timeout of the client, e.g. for long polls or server-sent events that go quiet for minutes. null, the default, keeps the read timeout of the client.
returnUnsentBody hands the request body back when the exchange fails before any connection was opened or acquired, i.e. the body was never read. The exchange fails with an UnprocessedRequestException whose takeUnsentBody() returns the body, so it can be sent to another server. Whoever takes the body owns it and must send or close it. A request that was redirected does not hand its body back: it was sent to the server that redirected it. Only the asynchronous raw client of the Netty client hands a body back; it is closed otherwise.
The following example sends a request to a primary server and, if no connection to it could be opened, sends the same body to a fallback server:
Retrying a raw exchange with the unsent body
The future returned by AsyncRawHttpClient.exchange owns the pending exchange. Cancelling it or completing it exceptionally (including orTimeout) aborts outstanding work and releases the request body. A response arriving after cancellation or timeout is closed. Cancelling or timing out a dependent stage leaves the original exchange running.
Once the response has arrived, the caller owns its body. Close the ByteBodyHttpResponse when finished, including when abandoning it after a later processing timeout. Cancelling an already completed exchange future does not release an active response body.
50 Declarative HTTP Clients with @Client
Now that you have an understanding of the workings of the lower-level HTTP client, let’s take a look at Micronaut’s support for declarative clients via the Client annotation.
Essentially, the @Client annotation can be declared on any interface or abstract class, and through the use of Introduction Advice the abstract methods are implemented for you at compile time, greatly simplifying the creation of HTTP clients.
Let’s start with a simple example. Given the following class:
Pet.java
public class Pet { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; }}
Note how the interface uses Micronaut’s HTTP annotations which are usable on both the server and client side. You can also use jakarta.validation constraints to validate arguments.
Tip
Be aware that some annotations, such as Produces and Consumes, have different semantics between server and client side usage. For example, @Produces on a controller method (server side) indicates how the method’s return value is formatted, while @Produces on a client indicates how the method’s parameters are formatted when sent to the server. While this may seem a little confusing, it is logical considering the different semantics between a server producing/consuming vs a client: a server consumes an argument and returns a response to the client, whereas a client consumes an argument and sends output to a server.
Additionally, to use the jakarta.validation features, add the validation module to your build:
@Controller("/pets")public class PetController implements PetOperations { @Override @SingleResult public Publisher<Pet> save(String name, int age) { Pet pet = new Pet(); pet.setName(name); pet.setAge(age); // save to database or something return Mono.just(pet); }}
@Controller("/pets")open class PetController : PetOperations { @SingleResult override fun save(name: String, age: Int): Publisher<Pet> { val pet = Pet() pet.name = name pet.age = age // save to database or something return Mono.just(pet) }}
@Controller("/pets")class PetController implements PetOperations { @Override @SingleResult Publisher<Pet> save(String name, int age) { Pet pet = new Pet(name: name, age: age) // save to database or something return Mono.just(pet) }}
You can then define a declarative client in src/test/java that uses @Client to automatically implement a client at compile time:
PetClient.java
Warning
Notice in the above example we override the save method. This is necessary if you compile without the -parameters option since Java does not retain parameters names in bytecode otherwise. Overriding is not necessary if you compile with -parameters. In addition, when overriding methods you should ensure any validation annotations are declared again since these are not Inherited annotations.
Once you have defined a client you can @Inject it wherever you need it.
A relative URI, in which case the server targeted is the current server (useful for testing)
A service identifier. See the section on Service Discovery for more information on this topic.
In production, you typically use a service ID and Service Discovery to discover services automatically.
Another important thing to notice regarding the save method in the example above is that it returns a Single type.
This is a non-blocking reactive type - typically you want your HTTP clients to not block. There are cases where you may want an HTTP client that does block (such as in unit tests), but this is rare.
The following table illustrates common return types usable with @Client:
Generally, any reactive type that can be converted to the Publisher interface is supported as a return type, including (but not limited to) the reactive types defined by RxJava 1.x, RxJava 2.x, and Reactor 3.x.
Returning CompletableFuture instances is also supported. Note that returning any other type results in a blocking request and is not recommended other than for testing.
50.1 Customizing Parameter Binding
The previous example presented a simple example using method parameters to represent the body of a POST request:
PetOperations.java
@Post@SingleResultPublisher<Pet> save(@NotBlank String name, @Min(1L) int age);
The save method performs an HTTP POST with the following JSON by default:
Example Produced JSON
{"name":"Dino", "age":10}
You may however want to customize what is sent as the body, the parameters, URI variables, etc. The @Client annotation is very flexible in this regard and supports the same io.micronaut.http.annotation as Micronaut’s HTTP server.
For example, the following defines a URI template, and the name parameter is used as part of the URI template, whilst @Body declares that the contents to send to the server are represented by the Pet POJO:
PetOperations.java
The following table summarizes the parameter annotations and their purpose, and provides an example:
Specifies parameters to be set as request attributes
@RequestAttribute Integer locationId
Important
Always use @Produces or @Consumes instead of supplying a header for Content-Type or Accept.
Query values formatting
The Format annotation can be used together with @QueryValue annotation to format query values.
The supported values are: "csv", "ssv", "pipes", "multi" and "deep-object", where the meaning is similar to Open API v3 query parameter’s style attribute.
The format can only be applied to java.lang.Iterable, java.util.Map or POJO with Introspected annotation. Examples of how different values will be formatted are given in the table below:
Format
Iterable example
Map or POJO example
Original value
["Mike", "Adam", "Kate"]
{"name": "Mike", "age": 30"}
"CSV"
"param=Mike,Adam,Kate"
"param=name,Mike,age,30"
"SSV"
"param=Mike Adam Kate"
"param=name Mike age 30"
"PIPES"
"param=Mike|Adam|Kate"
"param=name|Mike|age|30"
"MULTI"
"param=Mike¶m=Adam¶m=Kate"
"name=Mike&age=30"
"DEEP_OBJECT"
"param[0]=Mike¶m[1]=Adam¶m[2]=Kate"
"param[name]=Mike¶m[age]=30"
Type-Based Binding Parameters
Some parameters are recognized by their type instead of their annotation. The following table summarizes these parameter types and their purpose, and provides an example:
Sets the Accept-Language header. Annotate with @QueryValue or @PathVariable to populate a URI variable.
Locale locale
Custom Binding
The ClientArgumentRequestBinder API binds client arguments to the request. Custom binder classes registered as beans are automatically used during the binding process. Annotation-based binders are searched for first, with type-based binders being searched if a binder was not found.
Binding By Annotation
To control how an argument is bound to the request based on an annotation on the argument, create a bean of type AnnotatedClientArgumentRequestBinder. Any custom annotations must be annotated with @Bindable.
Without any additional code, the client attempts to convert the metadata to a string and append it as a query parameter. In this case that isn’t the desired effect, so a custom binder is needed.
The following binder handles arguments passed to clients that are annotated with the @Metadata annotation, and mutate the request to contain the desired headers. The implementation could be modified to accept more types of data other than Map.
Without any additional code, the client attempts to convert the metadata to a string and append it as a query parameter. In this case that isn’t the desired effect, so a custom binder is needed.
The following binder handles arguments passed to clients of type Metadata and mutate the request to contain the desired headers.
It is also possible to create a binder, that would change the request with an annotation on the method. For example:
Client With Annotated Method
The annotation is defined as:
Annotation Definition
The following binder specifies the behaviour:
Annotation Definition
50.2 Streaming with @Client
The @Client annotation can also handle streaming HTTP responses.
Streaming JSON with @Client
For example, to write a client that streams data from the controller defined in the JSON Streaming section of the documentation, you can define a client that returns an unbound Publisher such as Reactor’s Flux or a RxJava’s Flowable:
HeadlineClient.java
The following example shows how the previously defined HeadlineClient can be invoked from a test:
Streaming HeadlineClient
Streaming Clients and Response Types
The example defined in the previous section expects the server to respond with a stream of JSON objects, and the content type to be application/x-json-stream. For example:
A JSON Stream
{"title":"The Stand"}{"title":"The Shining"}
The reason for this is simple; a sequence of JSON object is not, in fact, valid JSON and hence the response content type cannot be application/json. For the JSON to be valid it would have to return an array:
A JSON Array
[ {"title":"The Stand"}, {"title":"The Shining"}]
Micronaut’s client does however support streaming of both individual JSON objects via application/x-json-stream and also JSON arrays defined with application/json.
If the server returns application/json and a non-single Publisher is returned (such as a Reactor’s Flux or a RxJava’s Flowable), the client streams the array elements as they become available.
Streaming without Reactive Streams
A declarative client method can also return a CompletionStage of BodyElements, a cursor that reads the elements one at a time, instead of a Publisher:
AsyncHeadlineClient.java
Reading the headlines
The elements are read like the items of a streaming publisher: the events of a text/event-stream response (BodyElements<Event<T>>, or BodyElements<T> for their data), the elements of a JSON stream or a JSON array, or the pieces of any other body (BodyElements<ByteBuffer<?>>). Declare CompletionStage<HttpResponse<BodyElements<T>>> to read the status and the headers of an event stream or of the pieces of a body as well. A Kotlin suspend method that returns BodyElements<T> works the same way.
When the response has an error status, the stage fails with an HttpClientResponseException whose response carries the error body. The stages complete on an I/O thread: do not block in them.
Streaming Clients and Read Timeout
When streaming responses from servers, the underlying HTTP client will not apply the default readTimeout setting (which defaults to 10 seconds) of the HttpClientConfiguration since the delay between reads for streaming responses may differ from normal reads.
Instead, the read-idle-timeout setting (which defaults to 5 minutes) dictates when to close a connection after it becomes idle.
If you stream data from a server that defines a longer delay than 5 minutes between items, you should adjust readIdleTimeout. The following configuration in your configuration file (e.g. application.yml) demonstrates how:
The above example sets the readIdleTimeout to ten minutes.
Streaming Server Sent Events
The Micronaut framework features a native client for Server Sent Events (SSE) defined by the interface SseClient.
You can use this client to stream SSE events from any server that emits them.
Note
Although SSE streams are typically consumed by a browser EventSource, there are cases where you may wish to consume an SSE stream via SseClient, such as in unit tests or when a Micronaut service acts as a gateway for another service.
The @Client annotation also supports consuming SSE streams. For example, consider the following controller method that produces a stream of SSE events:
SSE Controller
Notice that the return type of the controller is also Event and that the Event.of method creates events to stream to the client.
To define a client that consumes the events, define a method that processes MediaType.TEXT_EVENT_STREAM:
The generic type of the Flux can be either an Event, in which case you will receive the full event object, or a POJO, in which case you will receive only the data contained within the event converted from JSON.
Responses that are either a Single Body or an Event Stream
Some protocols let the server decide, per request, whether to answer with a single body or with an event stream. For example, with the MCP Streamable HTTP transport the client sends Accept: application/json, text/event-stream, and the server answers either with one JSON message or with text/event-stream. The client also reads response headers such as Mcp-Session-Id.
The exchangeEventStream methods of SseClient serve these protocols. They emit each event wrapped in an HttpResponse that exposes the status and the headers of the response:
When the response has an error status, the publisher fails with an HttpClientResponseException whose response carries the error body, decoded into the given error type, instead of completing empty.
Reading Events Asynchronously
The AsyncStreamingHttpClient reads the same responses with a CompletionStage instead of a Publisher, and without Reactor on the Netty client. Obtain it from a StreamingHttpClient with toAsyncStreaming(), or inject it with @Client:
Async Event Stream
The connection stays reserved until the events were read to the end or the BodyElements was closed, so close the events of a stream that you do not read to the end. The stages complete on an I/O thread: do not block in them. eventStream sets Accept: text/event-stream and completes with the events only.
50.3 Error Responses
If an HTTP response is returned with a code of 400 or higher, an HttpClientResponseException is created. The exception contains the original response. How that exception is thrown depends on the method return type.
For reactive response types, the exception is passed through the publisher as an error.
For blocking response types, the exception is thrown and should be caught and handled by the caller.
Important
The one exception to this rule is HTTP Not Found (404) responses. This exception only applies to the declarative client.
HTTP Not Found (404) responses for blocking return types is not considered an error condition and the client exception will not be thrown. That behavior includes methods that return void.
If the method returns an HttpResponse, the original response is returned. If the return type is Optional, an empty optional is returned. For all other types, null is returned.
When combining client calls with Retryable, all thrown exceptions will be retried by default. This will include all 4XX responses, except HTTP Not Found (404) as noted above. Specific retry criteria can be configured with a RetryPredicate to filter out responses that shouldn’t be retried.
50.4 Customizing Request Headers
Customizing the request headers deserves special mention as there are several ways that can be accomplished.
Populating Headers Using Configuration
The @Header annotation can be declared at the type level and is repeatable such that it is possible to drive the request headers sent via configuration using annotation metadata.
The above example defines a @Header annotation on the PetClient interface that reads the pet.client.id property using property placeholder configuration.
Then set the following in your configuration file (e.g. application.yml) to populate the value:
Configuring Headers
pet.client.id=foo
pet: client: id: foo
[pet.client]id = "foo"
pet { client { id = 'foo' }}
{pet = {client = {id = "foo" } }}
{ "pet": { "client": { "id": "foo" } }}
Alternatively you can supply a PET_CLIENT_ID environment variable and the value will be populated.
Populating Headers using a Client Filter
Alternatively, to dynamically populate headers, another option is to use a Client Filter.
For more information on writing client filters see the Client Filters section of the guide.
50.5 Customizing Jackson Settings
As mentioned previously, Jackson is used for message encoding to JSON. A default Jackson ObjectMapper is configured and used by Micronaut HTTP clients.
This section covers Jackson-specific configuration. If your code should remain portable across Micronaut JSON implementations, prefer depending on JsonMapper instead of Jackson’s ObjectMapper.
You can override the settings used to construct the ObjectMapper with properties defined by the JacksonConfiguration class in your configuration file (e.g application.yml).
For example, the following configuration enables indented output for Jackson:
However, these settings apply globally and impact both how the HTTP server renders JSON and how JSON is sent from the HTTP client. Given that, sometimes it is useful to provide client-specific Jackson settings. You can do this with the @JacksonFeatures annotation on a client:
As an example, the following snippet is taken from Micronaut’s native Eureka client (which of course uses Micronaut’s HTTP client):
The Eureka serialization format for JSON uses the WRAP_ROOT_VALUE serialization feature of Jackson, hence it is enabled just for that client.
Tip
If the customization offered by JacksonFeatures is not enough, you can also write a BeanCreatedEventListener for the ObjectMapper and add whatever customizations you need.
50.6 Retry and Circuit Breaker
Recovering from failure is critical for HTTP clients, and that is where Micronaut’s integrated Retry Advice comes in handy.
Note
Since Micronaut Framework 4.0, declarative clients annotated with @Client no longer invoke fallbacks by default. To restore the previous behaviour add the following dependency and annotate any declarative clients with @Recoverable.
For more information on customizing retry, see the section on Retry Advice.
50.7 Client Fallbacks
In distributed systems, failure happens, and it is best to be prepared for it and handle it gracefully.
In addition, when developing Microservices it is quite common to work on a single Microservice without other Microservices the project requires being available.
With that in mind, the Micronaut framework features a native fallback mechanism that is integrated into Retry Advice that allows falling back to another implementation in the case of failure.
Using the @Fallback annotation, you can declare a fallback implementation of a client to be used when all possible retries have been exhausted.
In fact the mechanism is not strictly linked to Retry; you can declare any class as @Recoverable, and if a method call fails (or, in the case of reactive types, an error is emitted) a class annotated with @Fallback will be searched for.
To illustrate this, consider again the PetOperations interface declared earlier. You can define a PetFallback class that will be called in the case of failure:
Defining a Fallback
@Fallbackpublic class PetFallback implements PetOperations { @Override @SingleResult public Publisher<Pet> save(String name, int age) { Pet pet = new Pet(); pet.setAge(age); pet.setName(name); return Mono.just(pet); }}
@Fallbackopen class PetFallback : PetOperations { override fun save(name: String, age: Int): Mono<Pet> { val pet = Pet() pet.age = age pet.name = name return Mono.just(pet) }}
@Fallbackclass PetFallback implements PetOperations { @Override Mono<Pet> save(String name, int age) { Pet pet = new Pet(age: age, name: name) return Mono.just(pet) }}
Tip
If you only need fallbacks to help with testing against external Microservices, you can define fallbacks in the src/test/java directory, so they are not included in production code. You will have to specify @Recoverable(api = PetOperations.class) on the declarative client if you are using fallbacks without hystrix.
As you can see the fallback does not perform any network operations and is quite simple, hence will provide a successful result in the case of an external system being down.
Of course, the actual behaviour of the fallback is up to you. You can for example implement a fallback that pulls data from a local cache when real data is not available, and sends alert emails or other notifications to operations about downtime.
51 HTTP Client Filters
Often, you need to include the same HTTP headers or URL parameters in a set of requests against a third-party API or when calling another Microservice.
To simplify this, you can define ClientFilter classes that are applied to all matching HTTP client requests. The details of how filters worked are described in the server filter documentation. Here we will only show some client filters.
As an example, say you want to build a client to communicate with the Bintray REST API. It would be tedious to specify authentication for every single HTTP call.
To resolve this you can define a filter. The following is an example BintrayService:
The Bintray API is secured. To authenticate you add an Authorization header for every request. You can modify fetchRepositories and fetchPackages methods to include the necessary HTTP Header for each request, but using a filter is much simpler:
Now when you invoke the bintrayService.fetchRepositories() method, the Authorization HTTP header is included in the request.
Injecting Another Client into a ClientFilter
To create an ReactorHttpClient, the Micronaut framework needs to resolve all ClientFilter beans, which creates a circular dependency when injecting another ReactorHttpClient or a @Client bean into an instance of a ClientFilter.
To resolve this issue, use the BeanProvider interface to inject another ReactorHttpClient or a @Client bean into an instance of ClientFilter.
For cases where a filter should be applied to a client regardless of the URL, filters can be matched by the presence of an annotation applied to both the filter and the client. Given the following client:
The following filter will filter the client requests:
The @BasicAuth annotation is just an example and can be replaced with your own.
52 HTTP/2 Support
By default, Micronaut’s HTTP client is configured to support HTTP 1.1. To enable support for HTTP/2, set the supported HTTP version in configuration:
Since Micronaut framework 4.x, Micronaut’s Netty-based HTTP client can be configured to support HTTP/3.
Instead of the TCP used for HTTP/1.1 and HTTP/2, HTTP/3 runs on UDP. If the client is configured with the special h3 value for the alpn-modes property, the client will automatically use HTTP/3 over UDP instead of HTTP/1.1 or HTTP/2 over TCP. At this time, the client cannot fall back to TCP if the server does not support HTTP/3.
Read the HTTP Client Guide (Java,
Groovy,
Kotlin), a step-by-step tutorial, to learn more.
55 Connection pooling in the netty client
The netty client uses a connection pool to share HTTP connections between requests for greater efficiency. The pools
are scoped by service (the service ID you can configure in the @Client annotation), and by authority (host and port),
so if you configure e.g. the maximum number of connections, what is meant is the number of connections per host.
You can configure the pool through the micronaut.http.client.pool.* properties or, for specific services, via
micronaut.http.services.*.pool.*. A service inherits the global pool settings, and any value set for that service
overrides the global one. I’ll only mention the simple property name from here on.
HTTP/1.1 and HTTP/2.0 connections are handled and configured separately, because the latter can handle many concurrent
requests on the same connection. For the purposes of pooling, HTTP/3 connections are treated as HTTP/2.0 connections.
When a request is initiated, it is added to a pending request queue. The maximum size of this queue is configured
with max-pending-acquires, the default is no limit. If the limit is exceeded, the request is rejected. You can also
configure an acquire-timeout (no timeout by default).
Once a request is in the pending request queue, it is dispatched to an existing connection. If all connections are
busy but there are still pending requests, a decision is made on whether to create new connections:
The maximum number of pending connections (i.e. connections that are still being established) may not exceed
max-pending-connections (default: 4).
For HTTP/1.1, the maximum number of connections may not exceed max-concurrent-http1-connections (no limit by
default).
For HTTP/2.0, the maximum number of connections may not exceed max-concurrent-http2-connections (default: 1).
There will be at most as many pending connections as there are pending requests.
The HTTP version is determined by any existing connections in the pool. If the pool is empty, both limits apply, so by
default only one connection is opened at first. For HTTP/2.0, there is also a
max-concurrent-requests-per-http2-connection setting that controls how many requests may run on the same HTTP/2.0
connection simultaneously.
Note that a pending connection is not actually associated to a request until it is fully established. A pending request
may time out, but the connection that was created for it will still enter the pool and may be used by another request.
Once a connection is in the pool, there are multiple ways it may terminate and exit the pool. During a request, the
connection may see a read timeout configured by read-timeout. When the connection is idle (no request), a similar
read timeout can be configured by connection-pool-idle-timeout. A fixed maximum connection lifetime may be configured
using connect-ttl, after which the connection will wind down (no new request will be sent and the connection will
terminate after all requests are done). Certain HTTP errors may also lead to a connection shutdown for safety.
56 Certificate providers
Certificate providers
Certificate providers let you externalize how TLS key and trust material is supplied to the framework. Each provider is a named bean and can be referenced from server and client SSL configuration using:
micronaut: ssl: enabled: true server: ssl: # Use the named provider "cert-a" for the server's key/cert key-name: cert-a key: password: "" http: client: ssl: # Trust the same material for the client trust-name: cert-a certificate: file: cert-a: format: pem path: /path/to/server.key.pem certificate-path: /path/to/server.crt.pem
For JKS/PKCS12, you can embed binary content via base64:…. For example: resource: "base64:<base64-encoded .p12/.jks bytes>" and set password if required.
Self-signed provider
The self-signed provider generates temporary certificates. It requires the netty-pkitesting dependency: