Docs navigation
1 The HTTP Server
HTTP 5.1.13
On this page

HTTP

1 The HTTP Server

Tip
Using the CLI

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:

implementation("io.micronaut:micronaut-http-server-netty")

A "Hello World" server application can be seen below:

1.1 Running the Embedded Server

To run the server, create an Application class with a static void main method, for example:

Micronaut Application Class
import io.micronaut.runtime.Micronaut;

public class Application {

    public static void main(String[] args) {
        Micronaut.run(Application.class);
    }
}

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.

1.2 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
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
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.

1.3 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

The actual implementation is handled by the UriMatchTemplate class, which extends UriTemplate.

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

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:

Table 2. HTTP Routing Annotations
Annotation HTTP Method

@Delete

DELETE

@Get

GET

@Head

HEAD

@Options

OPTIONS

@Patch

PATCH

@Put

PUT

@Post

POST

@Trace

TRACE

Note
All the method annotations default to /.

Route Conditions

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:

micronaut.server.dispatch-options-requests=true

Multiple URIs

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:

URI Variables Example
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.

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:

annotationProcessor("io.micronaut:micronaut-http-validation")

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:

String index(HttpRequest<?> request) {
    RouteMatch<?> routeMatch = RouteAttributes.getRouteMatch(request)
            .orElse(null);

1.4 Simple Request Binding

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:

Table 1. Parameter Binding Annotations
Annotation Description Example

@Body

Binds from the body of the request

@Body String body

@CookieValue

Binds a parameter from a cookie

@CookieValue String myCookie

@Header

Binds a parameter from an HTTP header

@Header String requestId

@QueryValue

Binds from a request query parameter

@QueryValue String myParam

@Part

Binds from a part of a multipart request

@Part CompletedFileUpload file

@RequestAttribute

Binds from an attribute of the request. Attributes are typically created in filters

@RequestAttribute String myAttribute

@PathVariable

Binds from the path of the request

@PathVariable String id

@RequestBean

Binds any Bindable value to single Bean object

@RequestBean MyBean bean

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:

@Get("/cookieName")
public String cookieName(@CookieValue("myCookie") String myCookie) {
    // ...
}

@Get("/cookieInferred")
public String cookieInferred(@CookieValue String myCookie) {
    // ...
}

Because hyphens are not allowed in variable names, it may be necessary to set the name in the annotation. The following definitions are equivalent:

@Get("/headerName")
public String headerName(@Header("Content-Type") String contentType) {
    // ...
}

@Get("/headerInferred")
public String headerInferred(@Header String contentType) {
    // ...
}

Stream Support

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:

Binding Request parameters to POJO
import io.micronaut.http.HttpStatus;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import org.jspecify.annotations.Nullable;

import jakarta.validation.Valid;

@Controller("/api")
public class BookmarkController {

    @Get("/bookmarks/list{?paginationCommand*}")
    public HttpStatus list(@Valid @Nullable PaginationCommand paginationCommand) {
        return HttpStatus.OK;
    }
}

Binding from Multiple Bindable values

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;
    }
}

which uses this bean class:

Bean definition
@Introspected
public class MovieTicketBean {

    private HttpRequest<?> httpRequest;

    @PathVariable
    private String movieId;

    @Nullable
    @QueryValue
    @PositiveOrZero
    private Double minPrice;

    @Nullable
    @QueryValue
    @PositiveOrZero
    private Double maxPrice;

    public MovieTicketBean(HttpRequest<?> httpRequest,
                           String movieId,
                           Double minPrice,
                           Double maxPrice) {
        this.httpRequest = httpRequest;
        this.movieId = movieId;
        this.minPrice = minPrice;
        this.maxPrice = maxPrice;
    }

    public HttpRequest<?> getHttpRequest() {
        return httpRequest;
    }

    public String getMovieId() {
        return movieId;
    }

    @Nullable
    public Double getMaxPrice() {
        return maxPrice;
    }

    @Nullable
    public Double getMinPrice() {
        return minPrice;
    }
}

The bean class has to be introspected with @Introspected. It can be one of:

  1. Mutable Bean class with setters and getters

  2. 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:

@Get("/headerInferred")
public String headerInferred(@Header String contentType) {
    // ...
}

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:

@Get("/headerNullable")
public String headerNullable(@Nullable @Header String contentType) {
    // ...
}

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:

@Get("/date")
public String date(@Header ZonedDateTime date) {
    // ...
}

@Get("/dateFormat")
public String dateFormat(@Format("dd/MM/yyyy hh:mm:ss a z") @Header ZonedDateTime date) {
    // ...
}

Type-Based Binding Parameters

Some parameters are recognized by their type instead of their annotation. The following table summarizes the parameter types, their purpose, and provides an example:

Type Description Example

BasicAuth

Allows binding of basic authorization credentials

BasicAuth basicAuth

Variable resolution

The Micronaut framework tries to populate method arguments in the following order:

  1. URI variables like /{id}.

  2. From query parameters if the request is a GET request (e.g. ?foo=bar).

  3. If there is a @Body and request allows the body, bind the body to it.

  4. 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).

  5. Finally, if the method arguments cannot be populated return 400 BAD REQUEST.

Binding Method Arguments From Body with no @Body
@Controller("/point")
public class PointController {

    @Post(uri = "/no-body-json")
    @Status(HttpStatus.CREATED)
    Point noBodyJson(Integer x, Integer y) { // (1)
        return new Point(x,y);
    }

    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    @Post("/no-body-form")
    @Status(HttpStatus.CREATED)
    Point noBodyForm(Integer x, Integer y) {  // (2)
        return new Point(x,y);
    }
}
  1. JSON request body binds to method controller arguments, e.g. '{"x":10,"y":20}' (with "application/json")

  2. Form data also works, e.g. 'x=10&y=20' (with "application/x-www-form-urlencoded")

1.5 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.

Note
At the end of processing if the result is still ArgumentBinder.BindingResult#UNSATISFIED, it is considered ArgumentBinder.BindingResult#EMPTY.

Key interfaces are:

AnnotatedRequestArgumentBinder

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
import io.micronaut.core.annotation.Introspected;

@Introspected
public 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

1.6 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:

  1. The supplied configuration

  2. The Forwarded header

  3. X-Forwarded- headers. If the X-Forwarded-Host header is not present, the other X-Forwarded headers are ignored.

  4. The Host header

  5. The properties on the request URI

  6. 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.

1.7 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.

1.8 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:

  1. The configured header

  2. The Forwarded header

  3. The X-Forwarded-For header

  4. The remote address on the request

The first priority header name can be configured with micronaut.server.client-address-header.

1.9 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:

Table 1. Bindable Micronaut Interfaces
Interface Description Example

HttpRequest

The full HttpRequest

String hello(HttpRequest request)

HttpHeaders

All HTTP headers present in the request

String hello(HttpHeaders headers)

HttpParameters

All HTTP parameters (either from URI variables or request parameters) present in the request

String hello(HttpParameters params)

Cookies

All Cookies present in the request

String hello(Cookies cookies)

Important
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:

Request and Response Example

The HttpRequest is also available from a static context via ServerRequestContext.

Using the ServerRequestContext
Important
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.

1.10 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.

@Get(value = "/http-response", produces = MediaType.TEXT_PLAIN)
public HttpResponse httpResponse() {
    return HttpResponse.status(HttpStatus.CREATED).body("success");
}

You can also use the @Status annotation.

@Status(HttpStatus.CREATED)
@Get(produces = MediaType.TEXT_PLAIN)
public String index() {
    return "success";
}

or even respond with an HttpStatus

@Get("/http-status")
public HttpStatus httpStatus() {
    return HttpStatus.CREATED;
}

Custom HTTP Status Codes

Micronaut supports arbitrary HTTP status codes not part of the predefined HttpStatus enum. You can use the API method HttpResponse#status(int,java.lang.String)

1.11 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.

1.12 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:

micronaut.codec.json.additional-types[0]=text/javascript
micronaut.codec.json.additional-types[1]=...

The currently supported configuration prefixes are json, json-stream, text, and text-stream.

1.13 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):

Configuring the IO thread pool
micronaut.executors.io.type=fixed
micronaut.executors.io.nThreads=75

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.

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

1.13.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!

1.13.2 Reactive Responses

The previous section introduced the notion of Reactive programming using Project Reactor and Micronaut.

The Micronaut framework supports returning common reactive types such as Mono (or Single Maybe Observable types from RxJava), an instance of Publisher or CompletableFuture from any controller method.

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.

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 RxJava subscribeOn(..) 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:

Table 1. Micronaut Response Types
Type Description Example Signature

Publisher

Any type that implements the Publisher interface

Publisher<String> hello()

CompletableFuture

A Java CompletableFuture instance

CompletableFuture<String> hello()

HttpResponse

An HttpResponse and optional response body

HttpResponse<Publisher<String>> hello()

CharSequence

Any implementation of CharSequence

String hello()

T

Any simple POJO type

Book show()

Note
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.

1.14 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.

1.14.1 Serialize using Micronaut Serialization

Micronaut Serialization offers reflection-free serialization using build-time Bean Introspections. It supports alternative formats such as JSON-P or JSON-B. You need to add the following dependencies:

annotationProcessor("io.micronaut.serde:micronaut-serde-processor")
implementation("io.micronaut.serde:micronaut-serde-jackson")

1.14.2 Serialization using Jackson Databind

To serialize using Jackson Databind include the following dependency:

implementation("io.micronaut:micronaut-jackson-databind")

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
@Introspected
data class Greeting(@JsonProperty("message") val message: String)

1.14.3 JsonMapper

You may be familiar with Jackson’s ObjectMapper. For implementation-agnostic application code, we recommend depending on JsonMapper, which is Micronaut’s JSON abstraction for common mapping operations. Micronaut Serialization and Micronaut Jackson Databind both provide implementations of JsonMapper, and Micronaut Serialization also defines its own ObjectMapper interface that extends JsonMapper.

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().

1.14.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}'

1.14.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);
                }
        );
    }

}

The above example uses the thenApply method to achieve the same as the previous example.

1.14.6 Binding using POJOs

Note however you can just as easily write:

Binding JSON POJOs
@Controller("/people")
public class PersonController {

    Map<String, Person> inMemoryDatastore = new ConcurrentHashMap<>();

    @Post
    public HttpResponse<Person> save(@Body Person person) {
        inMemoryDatastore.put(person.getFirstName(), person);
        return 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.

1.14.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.

dateFormat

String

The date format

locale

String

Uses Locale.forLanguageTag. Example: en-US

timeZone

String

Uses TimeZone.getTimeZone. Example: PST

serializationInclusion

String

One of JsonInclude.Include. Example: ALWAYS

propertyNamingStrategy

String

Name of an instance of PropertyNamingStrategy. Example: SNAKE_CASE

defaultTyping

String

The global defaultTyping for polymorphic type handling from enum ObjectMapper.DefaultTyping. Example: NON_FINAL

Example:

jackson.serializationInclusion=ALWAYS

1.14.7.1 Features

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.

Example:

jackson.serialization-features.indentOutput=true
jackson.serialization-features.writeDatesAsTimestamps=false
jackson.deserialization-features.useBigIntegerForInts=true
jackson.deserialization-features.failOnUnknownProperties=false

1.14.7.2 Further customising `JsonFactory`

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.

1.14.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.

1.14.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:

1.14.7.5 Service Loader

Any modules registered via the service loader are also added to the default object mapper.

1.14.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:

jackson.deserialization.useBigIntegerForInts=true
jackson.deserialization.useBigDecimalForFloats=true

1.15 Plain Text Responses

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.

1.16 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:

annotationProcessor("io.micronaut.validation:micronaut-validation-processor")
implementation("io.micronaut.validation:micronaut-validation")

Or full JSR 380 compliance with the micronaut-hibernate-validator dependency:

implementation("io.micronaut.beanvalidation:micronaut-hibernate-validator")

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:

Example Test
@Test
void testParametersAreValidated() {
    HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () ->
        client.toBlocking().exchange("/email/send?subject=Hi&recipient="));
    HttpResponse<?> response = e.getResponse();

    assertEquals(HttpStatus.BAD_REQUEST, response.getStatus());

    response = client.toBlocking().exchange("/email/send?subject=Hi&recipient=me@micronaut.example");

    assertEquals(HttpStatus.OK, response.getStatus());
}

To use your own ExceptionHandler to handle the constraint exceptions, annotate it with @Replaces(ConstraintExceptionHandler.class)

Often you may want to use POJOs as controller method parameters.

Annotate your controller with Validated, and annotate the binding POJO with @Valid.

Example

Validation of POJOs is shown in the following test:

@Test
void testPojoValidation() {
    HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> {
        Email email = new Email();
        email.subject = "Hi";
        email.recipient = "";
        client.toBlocking().exchange(HttpRequest.POST("/email/send", email));
    });
    HttpResponse<?> response = e.getResponse();

    assertEquals(HttpStatus.BAD_REQUEST, response.getStatus());

    Email email = new Email();
    email.subject = "Hi";
    email.recipient = "me@micronaut.example";
    response = client.toBlocking().exchange(HttpRequest.POST("/email/send", email));

    assertEquals(HttpStatus.OK, response.getStatus());
}
Note
Bean injection is supported in custom constraints with the Hibernate Validator configuration.

1.16.1 Validation Groups

You can enforce a subset of constraints using validation groups using groups on Validated. More information is available in the Bean Validation specification

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:

@Test
void testPojoValidation_defaultGroup() {
    HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> {
        Email email = new Email();
        email.subject = "";
        email.recipient = "";
        client.toBlocking().exchange(HttpRequest.POST("/email/createDraft", email));
    });
    HttpResponse<?> response = e.getResponse();

    assertEquals(HttpStatus.BAD_REQUEST, response.getStatus());

    Email email = new Email();
    email.subject = "Hi";
    email.recipient = "";
    response = client.toBlocking().exchange(HttpRequest.POST("/email/createDraft", email));

    assertEquals(HttpStatus.OK, response.getStatus());
}

Validation of POJOs using the custom FinalValidation validation group is shown in the following test:

@Test
void testPojoValidation_finalValidationGroup() {
    HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> {
        Email email = new Email();
        email.subject = "Hi";
        email.recipient = "";
        client.toBlocking().exchange(HttpRequest.POST("/email/send", email));
    });
    HttpResponse<?> response = e.getResponse();

    assertEquals(HttpStatus.BAD_REQUEST, response.getStatus());

    Email email = new Email();
    email.subject = "Hi";
    email.recipient = "me@micronaut.example";
    response = client.toBlocking().exchange(HttpRequest.POST("/email/send", email));

    assertEquals(HttpStatus.OK, response.getStatus());
}

1.17 Serving Static Resources

Static resource resolution is enabled by default. The Micronaut framework supports resolving resources from the classpath or the file system.

See the information below for available configuration options:

Tip
Read the Serving static resources in a Micronaut Application guide, a step-by-step tutorial, to learn how to expose static resources such as CSS or images in a Micronaut Framework application.

1.18 Error Handling

Sometimes with distributed applications, bad things happen. Having a good way to handle errors is important.

1.18.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.
Tip
See the guide for Error Handling to learn more.

1.18.2 Local Error Handling

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.

1.18.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.

1.18.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.

1.18.4.1 Built-In Exception Handlers

The Micronaut framework ships with several built-in handlers:

Exception

Handler

jakarta.validation.ConstraintViolationException

ConstraintExceptionHandler

ContentLengthExceededException

ContentLengthExceededHandler

ConversionErrorException

ConversionErrorHandler

DuplicateRouteException

DuplicateRouteHandler

HttpStatusException

HttpStatusHandler

UnsupportedMediaException

HttpStatusHandler

NotFoundException

HttpStatusHandler

NotAcceptableException

HttpStatusHandler

NotAllowedException

NotAllowedExceptionHandler

com.fasterxml.jackson.core.JsonProcessingException

JsonExceptionHandler

java.net.URISyntaxException

URISyntaxHandler

UnsatisfiedArgumentException

UnsatisfiedArgumentHandler

UnsatisfiedRouteException

UnsatisfiedRouteHandler

1.18.4.2 Custom Exception Handler

Imagine your e-commerce app throws an OutOfStockException when a book is out of stock:

public class OutOfStockException extends RuntimeException {
}

Along with BookController:

@Controller("/books")
public class BookController {

    @Produces(MediaType.TEXT_PLAIN)
    @Get("/stock/{isbn}")
    Integer stock(String isbn) {
        throw new OutOfStockException();
    }
}

The server returns a 500 (Internal Server Error) status code if you don’t handle the exception.

To respond with 400 Bad Request as the response when the OutOfStockException is thrown, you can register a ExceptionHandler:

1.18.5 Error Formatting

The Micronaut framework produces error responses via a bean of type ErrorResponseProcessor.

JSON error responses are provided with a bean of type JsonErrorResponseBodyProvider. The default implementation outputs vnd.error responses.

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 existe
404.error.title=No encontrado
404.error=Es posible que haya escrito mal la dirección o que la página se haya movido.

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.

1.19 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):

Enabling Versioning
micronaut.router.versioning.enabled=true

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:

Configuring Versioning
micronaut.router.versioning.enabled=true
micronaut.router.versioning.parameter.enabled=false
micronaut.router.versioning.parameter.names=v,api-version
micronaut.router.versioning.header.enabled=true
micronaut.router.versioning.header.names[0]=X-API-VERSION
micronaut.router.versioning.header.names[1]=Accept-Version
  • This example enables versioning

  • 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.

Configuring Default Version
micronaut.router.versioning.enabled=true
micronaut.router.versioning.default-version=3
  • This example enables versioning and sets the default version

A route is not matched if the following conditions are met:

  • The default version is configured

  • No version is found in the request

  • The route defines a version

  • The route version does not match the default version

If the incoming request specifies a version, the default version has no effect.

Versioning Client Requests

Micronaut’s Declarative HTTP client also supports automatic versioning of outgoing requests via the @Version annotation.

By default, if you annotate a client interface with @Version, the value supplied to the annotation is included using the X-API-VERSION header.

For example:

The default behaviour for how the version is sent for each call can be configured with DefaultClientVersioningConfiguration:

For example to use Accept-Version as the header name:

Configuring Client Versioning
micronaut.http.client.versioning.default.headers[0]=Accept-Version
micronaut.http.client.versioning.default.headers[1]=X-API-VERSION

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:

Configuring Versioning
micronaut.http.client.versioning.greeting-service.headers[0]=Accept-Version
micronaut.http.client.versioning.greeting-service.headers[1]=X-API-VERSION

The above uses a key named greeting-service which can be used to configure a client annotated with @Client('greeting-service').

1.20 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:

Binding Form Data to POJOs
@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);
        return 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)

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);
    }

}

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.

1.21 Forms

The Micronaut HTTP server includes support for forms, both using multipart/form-data and using application/x-www-form-urlencoded format.

1.21.1 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.

1.22 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 Publisher onComplete 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.

Performing Blocking I/O With InputStream
@Get(value = "/write", produces = MediaType.TEXT_PLAIN)
InputStream write() {
    byte[] bytes = "test".getBytes(StandardCharsets.UTF_8);
    return new ByteArrayInputStream(bytes); // 
}
  1. 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.

1.23 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:

@Post(consumes = MediaType.MULTIPART_FORM_DATA)
HttpResponse upload( ... )

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.

Receiving a byte array
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

import static io.micronaut.http.MediaType.MULTIPART_FORM_DATA;
import static io.micronaut.http.MediaType.TEXT_PLAIN;

@Controller("/upload")
public class BytesUploadController {

    @Post(value = "/bytes", consumes = MULTIPART_FORM_DATA, produces = TEXT_PLAIN) // 
    public HttpResponse<String> uploadBytes(byte[] file, String fileName) { // 
        try {
            File tempFile = File.createTempFile(fileName, "temp");
            Path path = Paths.get(tempFile.getAbsolutePath());
            Files.write(path, file); // 
            return HttpResponse.ok("Uploaded");
        } catch (IOException e) {
            return HttpResponse.badRequest("Upload Failed");
        }
    }
}

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:

For example:

HttpResponse upload(Publisher<StreamingFileUpload> files)
HttpResponse upload(Publisher<CompletedFileUpload> files)
HttpResponse upload(Publisher<MyObject> files)
HttpResponse upload(Publisher<Publisher<PartData>> files)
HttpResponse upload(Publisher<CompletedPart> attributes)

Whole Body Binding

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.

For example:

Binding to the entire multipart body
import io.micronaut.core.async.annotation.SingleResult;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.multipart.CompletedFileUpload;
import io.micronaut.http.multipart.CompletedPart;
import io.micronaut.http.server.multipart.MultipartBody;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

import java.io.IOException;

import static io.micronaut.http.MediaType.MULTIPART_FORM_DATA;
import static io.micronaut.http.MediaType.TEXT_PLAIN;

@Controller("/upload")
public class WholeBodyUploadController {

    @Post(value = "/whole-body", consumes = MULTIPART_FORM_DATA, produces = TEXT_PLAIN) // 
    @SingleResult
    public Publisher<String> uploadBytes(@Body MultipartBody body) { // 

        return Mono.create(emitter -> {
            Flux.from(body).publishOn(Schedulers.boundedElastic()).subscribe(new Subscriber<>() {
                private Subscription s;

                @Override
                public void onSubscribe(Subscription s) {
                    this.s = s;
                    s.request(1);
                }

                @Override
                public void onNext(CompletedPart completedPart) {
                    String partName = completedPart.getName();
                    if (completedPart instanceof CompletedFileUpload upload) {
                        String originalFileName = upload.getFilename();
                    }
                    try {
                        completedPart.close();
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }

                @Override
                public void onError(Throwable t) {
                    emitter.error(t);
                }

                @Override
                public void onComplete() {
                    emitter.success("Uploaded");
                }
            });
        });
    }
}

1.24 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
@Get
public 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
@Get
public 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.

Sending Reactive Stream
NOTE: Missing tag `endclass` in `test-suite/src/test/java/io/micronaut/docs/server/transfer/DownloadController.java`.
Note
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.

1.25 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.

Filters support the following use cases:

  • Decoration of the incoming HttpRequest

  • Modification of the outgoing HttpResponse

  • Implementation of cross-cutting concerns such as security, tracing, etc.

There are two ways to implement a filter:

Note
We recommend Micronaut developers use Filter methods introduced in Micronaut Framework 4.0 to implement filters.

1.25.1 Filter Patterns

Filter patterns can be defined on the filter class (in Filter, the ServerFilter or ClientFilter annotation), or on the filter method (in the RequestFilter or ResponseFilter annotation).

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:

  • ? matches one character

  • * matches zero or more characters

  • ** matches zero or more subdirectories in a path

Table 1. @Filter Annotation Path Matching Examples
Pattern Example Matched Paths

/**

any path

customer/j?y

customer/joy, customer/jay

customer/*/id

customer/adam/id, customer/amy/id

customer/**

customer/adam, customer/adam/id, customer/adam/name

customer/*/.html

customer/index.html, customer/adam/profile.html, customer/adam/job/description.html

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.

1.25.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.

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.

1.25.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.

1.25.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.

@ServerFilter(ServerFilter.MATCH_ALL_PATTERN)
class FooBarFilter {
    @RequestFilter
    CompletableFuture<@Nullable HttpResponse<?>> filter(HttpRequest<?> request) {
        if (request.getHeaders().contains("X-FOOBAR")) {
            // proceed
            return CompletableFuture.completedFuture(null);
        } else {
            return CompletableFuture.completedFuture(HttpResponse.unauthorized());
        }
    }
}

1.25.2.3 Error States

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

void responseFilter(HttpResponse<?> response, @Nullable Throwable failure)

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.

1.25.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.

1.25.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.

  1. An Order or @Priority annotation on the filter method

  2. An Order or @Priority annotation on the filter class

  3. 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.

Micronaut applies the same numeric precedence rule to jakarta.annotation.Priority because @Priority is mapped to @Order during annotation processing.

filter 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.

1.25.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 doFilter method accepts the HttpRequest and an instance of ServerFilterChain.

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.

1.25.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

1.25.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.

1.26 Advanced body access

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.

1.26.1 Primary operations

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.

1.26.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.

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.

1.26.3 Example

This example adds a filter that will log the body bytes as they come in.

A simple controller
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.annotation.Introspected;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Controller("/person")
public class BodyLogController {
    private static final Logger LOG = LoggerFactory.getLogger(BodyLogController.class);

    @Post
    void create(@Body Person person) { // 
        LOG.info("Creating person {}", person);
    }

    @Introspected
    record Person(String firstName, String lastName) {
    }
}
Logging filter
import io.micronaut.context.annotation.Requires;
import io.micronaut.http.ServerHttpRequest;
import io.micronaut.http.annotation.RequestFilter;
import io.micronaut.http.annotation.ServerFilter;
import io.micronaut.http.body.ByteBody;
import io.micronaut.http.body.CloseableByteBody;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;

import java.util.Base64;

@ServerFilter("/person")
public class BodyLogFilter {
    private static final Logger LOG = LoggerFactory.getLogger(BodyLogFilter.class);

    @RequestFilter
    public void logBody(ServerHttpRequest<?> request) { // 
        try (CloseableByteBody ourCopy = // 
                 request.byteBody()
                     .split(ByteBody.SplitBackpressureMode.SLOWEST) // 
                     .allowDiscard()) { // 
            Flux.from(ourCopy.toByteArrayPublisher()) // 
                .onErrorComplete(ByteBody.BodyDiscardedException.class) // 
                .subscribe(array -> LOG.info("Received body: {}", Base64.getEncoder().encodeToString(array))); // 
        }
    }
}
  1. 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.

1.27 HTTP Sessions

See the documentation for Micronaut Session for information about supporting HTTP sessions in your applications.

1.28 Server Sent Events

The Micronaut HTTP server supports emitting Server Sent Events (SSE) using the Event API.

To emit events from the server, return a Reactive Streams Publisher that emits objects of type Event.

The Publisher itself could publish events from a background task, via an event system, etc.

Imagine for an example an event stream of news headlines; you may define a data class as follows:

Headline
public class Headline {

    private String title;
    private String description;

    public Headline() {}

    public Headline(String title, String description) {
        this.title = title;
        this.description = description;
    }

    public String getTitle() {
        return title;
    }

    public String getDescription() {
        return description;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public void setDescription(String description) {
        this.description = description;
    }
}

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.

1.29 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:
implementation("io.micronaut:micronaut-websocket")

1.29.1 Using @ServerWebSocket

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
Tip
A working example of WebSockets in action can be found at Micronaut Guides.

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)

  • An instance of WebSocketSession

The @OnClose Method

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:

WebSocket Chat Example
@OnMessage
public Publisher<Message> onMessage(String topic, String username,
                                    Message message, WebSocketSession session) {
    String text = "[" + username + "] " + message.getText();
    Message newMessage = new Message(text);
    return broadcaster.broadcast(newMessage, isValid(topic, session));
}

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):

Setting the Connection Timeout for the Server
micronaut.server.idle-timeout=30m

If you use Micronaut’s WebSocket client you may also wish to set the timeout on the client:

Setting the Connection Timeout for the Client
micronaut.http.client.read-idle-timeout=30m

1.29.2 Using @ClientWebSocket

The @ClientWebSocket annotation can be used with the WebSocketClient interface to define WebSocket clients.

You can inject a reference to a WebSocketClient using the @Client annotation:

@Inject
@Client("http://localhost:8080")
WebSocketClient webSocketClient;

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:

Connecting a Client WebSocket
ChatClientWebSocket chatClient = webSocketClient
    .connect(ChatClientWebSocket.class, "/chat/football/fred")
    .blockFirst();
chatClient.send("Hello World!");
Note
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.
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

1.30 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:

Enabling HTTP/2 Support
micronaut.server.http-version=2

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:

Enabling h2 Protocol Support
micronaut.server.http-version=2
micronaut.server.ssl.enabled=true
micronaut.server.ssl.buildSelfSigned=true

For production, see the configuring HTTPS section of the documentation.

Note that if your deployment environment uses JDK 8, or for improved support for OpenSSL, define the following dependencies on Netty Tomcat Native:

runtimeOnly("io.netty:netty-tcnative:2.0.58.Final")
runtimeOnly("io.netty:netty-tcnative-boringssl-static:2.0.58.Final")

In addition to a dependency on the appropriate native library for your architecture. For example:

Configuring Tomcat Native
runtimeOnly "io.netty:netty-tcnative-boringssl-static:2.0.58.Final:${Os.isFamily(Os.FAMILY_MAC) ? (Os.isArch("aarch64") ? "osx-aarch_64" : "osx-x86_64") : 'linux-x86_64'}"

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.

Note
PushCapableHttpRequest extends HttpRequest so it’s not necessary to have both as arguments in a controller method.

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"))).

1.31 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:

Enabling HTTP/3 Support
micronaut:
  server:
    netty:
      listeners:
        http3Listener:
          family: QUIC
          port: 8443
Note
that defining this listener will disable the implicit TCP listeners. You can add them manually as described in the listener section.

Additionally, the netty HTTP/3 codec needs to be present on the classpath:

implementation("io.micronaut:micronaut-http-netty-http3")

1.32 Server Events

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:

Table 1. Server Events
Event Description

ServerStartupEvent

Emitted when the server completes startup

ServerShutdownEvent

Emitted when the server shuts down

ServiceReadyEvent

Emitted after all ServerStartupEvent listeners have been invoked and exposes the EmbeddedServerInstance

ServiceStoppedEvent

Emitted after all ServerShutdownEvent listeners have been invoked and exposes the EmbeddedServerInstance

Warning
Doing significant work within a listener for a ServerStartupEvent will increase startup time.

The following example defines a ApplicationEventListener that listens for ServerStartupEvent:

Listening for Server Startup Events
import io.micronaut.context.event.ApplicationEventListener;
...
@Singleton
public class StartupListener implements ApplicationEventListener<ServerStartupEvent> {
    @Override
    public void onApplicationEvent(ServerStartupEvent event) {
        // logic here
        ...
    }
}

Alternatively, you can also use the @EventListener annotation on a method of any bean that accepts ServerStartupEvent:

Using @EventListener with ServerStartupEvent
import io.micronaut.runtime.event.annotation.EventListener;
import io.micronaut.runtime.server.event.ServerStartupEvent;
import jakarta.inject.Singleton;
...
@Singleton
public class MyBean {

    @EventListener
    public void onStartup(ServerStartupEvent event) {
        // logic here
        ...
    }
}

1.33 Configuring the HTTP Server

The HTTP server features a number of configuration options. They are defined in the NettyHttpServerConfiguration configuration class, which extends HttpServerConfiguration.

The following example shows how to tweak configuration options for the server via your configuration file (e.g application.yml):

Configuring HTTP server settings
micronaut.server.maxRequestSize=1MB
micronaut.server.host=localhost
micronaut.server.netty.maxHeaderSize=500KB
micronaut.server.netty.worker.threads=8
micronaut.server.netty.childOptions.autoRead=true
  • By default, Micronaut framework binds to all network interfaces. Use localhost to bind only to loopback network interface

  • maxHeaderSize sets the maximum header size

  • worker.threads specifies the number of Netty worker threads

  • autoRead enables request body auto read

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:

For macOS on x86:

runtimeOnly("io.netty:netty-transport-native-kqueue::osx-x86_64")

For macOS on M1:

runtimeOnly("io.netty:netty-transport-native-kqueue::osx-aarch_64")

For Linux on x86:

runtimeOnly("io.netty:netty-transport-native-epoll::linux-x86_64")

or

runtimeOnly("io.netty:netty-transport-native-io_uring::linux-x86_64")

For Linux on ARM64:

runtimeOnly("io.netty:netty-transport-native-epoll::linux-aarch_64")

or

runtimeOnly("io.netty:netty-transport-native-io_uring::linux-aarch_64")

Then configure the default event loop group to prefer native transports:

Configuring The Default Event Loop to Prefer Native Transports
micronaut.netty.event-loops.default.transport=io_uring,epoll,kqueue,nio

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.

1.33.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:

Using a different event loop for the server
micronaut.server.netty.worker.event-loop-group=other
micronaut.netty.event-loops.other.num-threads=10
Note
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:

1.33.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:

Configuring the Server I/O Thread Pool
micronaut.executors.io.type=fixed
micronaut.executors.io.nThreads=75

The above configuration creates a fixed thread pool with 75 threads.

1.33.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:

Configuring the Server I/O Thread Pool
@Controller("/hello")
class HelloWorldController {

    @ExecuteOn(TaskExecutors.BLOCKING)
    @Produces(MediaType.TEXT_PLAIN)
    @Get("/world")
    String index() {
        return "Hello World";
    }
}

Event loop carrier

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

Enabling the event loop carrier
micronaut.netty.event-loops.default.loom-carrier=true

1.33.1.3 @Blocking

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:

Blocking Type Description

BlockingHttpClient

Intended for testing, provides blocking versions for a subset of HttpClient operations.

IOUtils

Reads the contents of a BufferedReader in a blocking manner, and returns that as a String.

BootstrapPropertySourceLocator

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.

1.33.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

1.33.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.

1.33.4 Advanced Listener Configuration

Instead of configuring a single port, you can also specify each listener manually.

micronaut.server.netty.listeners.httpListener.host=127.0.0.1
micronaut.server.netty.listeners.httpListener.port=8086
micronaut.server.netty.listeners.httpListener.ssl=false
micronaut.server.netty.listeners.httpsListener.port=8087
micronaut.server.netty.listeners.httpsListener.ssl=true
  • httpListener is a listener name, and can be an arbitrary value

  • host is optional, and by default binds to all interfaces

Warning
If you specify listeners manually, other configuration such as micronaut.server.port will be ignored.

SSL can be enabled or disabled for each listener individually. When enabled, the SSL will be configured as described above.

The embedded server also supports binding to unix domain sockets using netty. This requires the following dependency:

implementation("io.netty:netty-transport-native-unix-common")

The server must also be configured to use native transport (epoll or kqueue).

micronaut.server.netty.listeners.unixListener.family=UNIX
micronaut.server.netty.listeners.unixListener.path=/run/micronaut.socket
micronaut.server.netty.listeners.unixListener.ssl=true
  • 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.parent.transport=epoll
micronaut.netty.event-loops.default.transport=epoll
micronaut.netty.listeners.systemd.fd=3
micronaut.netty.listeners.systemd.bind=false

Example app.service file:

[Unit]
Description=Micronaut HTTP server with socket activation
After=network.target app.socket
Requires=app.socket

[Service]
Type=simple
ExecStart=/usr/bin/java -jar /app.jar

Example app.socket file:

[Socket]
ListenStream=127.0.0.1:8080

[Install]
WantedBy=sockets.target

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
micronaut.netty.listeners.systemd.bind=false

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.

1.33.5 Configuring CORS

The Micronaut framework supports CORS (Cross Origin Resource Sharing) out of the box. By default, CORS requests are rejected.

Tip
See the guide for Configure CORS in a Micronaut Application to learn more.

1.33.5.1 Annotation-based CORS Configuration

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.

@CrossOrigin(
	allowedOrigins = { "http://foo.com" },
	allowedOriginsRegex = "^http(|s):\\/\\/www\\.google\\.com$",
	allowedMethods = { HttpMethod.POST, HttpMethod.PUT },
	allowedHeaders = { HttpHeaders.CONTENT_TYPE, HttpHeaders.AUTHORIZATION },
	exposedHeaders = { HttpHeaders.CONTENT_TYPE, HttpHeaders.AUTHORIZATION },
	allowCredentials = false,
	allowPrivateNetwork = false,
	maxAge = 3600
)

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.

1.33.5.2 CORS via Configuration

To enable processing of CORS requests, modify your configuration in the application configuration file:

CORS Configuration Example
micronaut.server.cors.enabled=true

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.

CORS Configurations
micronaut.server.cors.enabled=true
micronaut.server.cors.configurations.all=...
micronaut.server.cors.configurations.web=...
micronaut.server.cors.configurations.mobile=...

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.

1.33.5.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.

Example CORS Configuration
micronaut.server.cors.enabled=true
micronaut.server.cors.configurations.web.allowed-origins-regex=^http(|s):\/\/www\.google\.com$
micronaut.server.cors.configurations.web.allowed-origins[0]=http://foo.com
Warning
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.

1.33.5.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.

Example CORS Configuration
micronaut.server.cors.enabled=true
micronaut.server.cors.configurations.web.allowed-methods[0]=POST
micronaut.server.cors.configurations.web.allowed-methods[1]=PUT

1.33.5.5 Allowed Headers

To allow any request header for a given configuration, don’t include the allowed-headers key in your configuration.

For multiple allowed headers, set the allowed-headers key of the configuration to a list of strings.

Example CORS Configuration
micronaut.server.cors.enabled=true
micronaut.server.cors.configurations.web.allowed-headers[0]=Content-Type
micronaut.server.cors.configurations.web.allowed-headers[1]=Authorization

1.33.5.6 Exposed Headers

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.

Example CORS Configuration
micronaut.server.cors.enabled=true
micronaut.server.cors.configurations.web.exposed-headers[0]=Content-Type
micronaut.server.cors.configurations.web.exposed-headers[1]=Authorization

1.33.5.7 Allow Credentials

Credentials are disabled by default for CORS requests. To allow credentials, set the allow-credentials option to true.

Example CORS Configuration
micronaut.server.cors.enabled=true
micronaut.server.cors.configurations.web.allow-credentials=true

1.33.5.8 Allow Private Network

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.

Example CORS Configuration
micronaut.server.cors.enabled=true
micronaut.server.cors.configurations.web.allow-private-network=false

1.33.5.9 Max Age

The default maximum age that preflight requests can be cached is 30 minutes. To change that behavior, specify a value in seconds.

Example CORS Configuration
micronaut.server.cors.enabled=true
micronaut.server.cors.configurations.web.max-age=3600

1.33.5.10 Multiple Header Values

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

1.33.6 Securing the Server with HTTPS

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)
micronaut.server.ssl.enabled=true
micronaut.server.ssl.key-name=cert-a
micronaut.certificate.self-signed.cert-a=
  • The Micronaut framework will create a self-signed certificate.

Tip
By default, a Micronaut application with HTTPS support starts on port 8443 but you can change the port with the property micronaut.server.ssl.port.

For generating self-signed certificates, the Micronaut HTTP server will use netty-pkitesting, which requires this dependency:

runtimeOnly("io.netty:netty-pkitesting")
Warning
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.

HTTPS Configuration Example
micronaut.server.ssl.enabled=true
micronaut.server.ssl.key-name=cert-a
micronaut.certificate.file.cert-a.format=pem
micronaut.certificate.file.cert-a.path=server.key
micronaut.certificate.file.cert-a.certificate-path=server.crt

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.

https certificate

Finally, we can test that the certificate is valid for the browser by adding an alias to the domain in /etc/hosts file:

$ cat /etc/hosts
...
127.0.0.1   my-domain.org
...

Now we can connect to https://my-domain.org:8443:

https valid certificate

Using a PKCS#12 key store

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:

HTTPS Configuration Example
micronaut.server.ssl.enabled=true
micronaut.server.ssl.key-name=cert-a
micronaut.certificate.resource.cert-a.resource=classpath:server.p12
micronaut.certificate.resource.cert-a.password=mypassword
  • Specify the p12 file path

  • Also provide the password defined during the export

Using a JKS key store

You can optionally convert the p12 store to a JKS one:

Warning
If either srcstorepass or alias are not the same as defined in the p12 file, the conversion will fail.

Now modify your configuration:

HTTPS Configuration Example
micronaut.server.ssl.enabled=true
micronaut.server.ssl.key-name=cert-a
micronaut.certificate.resource.cert-a.resource=classpath:server.keystore
micronaut.certificate.resource.cert-a.password=newPassword

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.

1.33.7 Enabling HTTP and HTTPS

The Micronaut framework supports binding both HTTP and HTTPS. To enable dual protocol support, modify your configuration. For example:

Dual Protocol Configuration Example
micronaut.server.ssl.enabled=true
micronaut.server.ssl.build-self-signed=true
micronaut.server.dual-protocol=true
  • 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:

Enable HTTP to HTTPS Redirects
micronaut.server.ssl.enabled=true
micronaut.server.ssl.build-self-signed=true
micronaut.server.dual-protocol=true
micronaut.server.http-to-https-redirect=true
  • http-to-https-redirect enables HTTP to HTTPS redirects

1.33.8 Enabling Access Logger

In the spirit of apache mod_log_config and Tomcat Access Log Valve, it is possible to enable an access logger for the HTTP server (this works for both HTTP/1 and HTTP/2).

To enable and configure the access logger, in your configuration file (e.g application.yml) set:

Enabling the access logger
micronaut.server.netty.access-logger.enabled=true
micronaut.server.netty.access-logger.logger-name=my-access-logger
micronaut.server.netty.access-logger.log-format=common
  • enabled Enables the access logger

  • optionally specify a logger-name, which defaults to HTTP_ACCESS_LOGGER

  • optionally specify a log-format, which defaults to the Common Log Format

Filtering access logs

If you wish to not log access to certain paths, you can specify regular expression filters in the configuration:

Filtering the access logs
micronaut.server.netty.access-logger.enabled=true
micronaut.server.netty.access-logger.logger-name=my-access-logger
micronaut.server.netty.access-logger.log-format=common
micronaut.server.netty.access-logger.exclusions[0]=/health
micronaut.server.netty.access-logger.exclusions[1]=/path/.+
  • enabled Enables the access logger

  • 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:

Logback configuration
<appender
    name="httpAccessLogAppender"
    class="ch.qos.logback.core.rolling.RollingFileAppender">
    <append>true</append>
    <file>log/http-access.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <!-- daily rollover -->
        <fileNamePattern>log/http-access-%d{yyyy-MM-dd}.log
        </fileNamePattern>
        <maxHistory>7</maxHistory>
    </rollingPolicy>
    <encoder>
        <charset>UTF-8</charset>
        <pattern>%msg%n</pattern>
    </encoder>
    <immediateFlush>true</immediateFlush>
</appender>

<logger name="HTTP_ACCESS_LOGGER" additivity="false" level="info">
    <appender-ref ref="httpAccessLogAppender" />
</logger>

The pattern should only have the message marker, as other elements will be processed by the access logger.

Log Format

The syntax is based on Apache httpd log format.

These are the supported markers:

  • %a - Remote IP address

  • %A - Local IP address

  • %b - Bytes sent, excluding HTTP headers, or '-' if no bytes were sent

  • %B - Bytes sent, excluding HTTP headers

  • %h - Remote host name

  • %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

  • %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:

1.33.9 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

1.34 Server Side View Rendering

The Micronaut framework supports Server Side View Rendering.

See the documentation for Micronaut Views for more information.

1.35 OpenAPI / Swagger Support

To configure the Micronaut integration with OpenAPI/Swagger, please follow these instructions

1.36 GraphQL Support

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.

2 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.

2.1 HTTP Client Implementations

There are several implementations of the Micronaut HTTP Client.

2.1.1 HTTP Client based on Netty

To use an implementation based on Netty, add the following dependency to your build:

implementation("io.micronaut:micronaut-http-client")

Disable hostname verification in ssl config

It may be necessary in a testing or development environment to disable hostname verification.

To disable hostname verification in your test classpath, you could create a Bean Replacement in src/test/java:

package io.micronaut.http.client.netty.ssl;

import io.micronaut.context.annotation.BootstrapContextCompatible;
import io.micronaut.context.annotation.Replaces;
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.annotation.Secondary;
import io.micronaut.context.env.Environment;
import io.micronaut.core.io.ResourceResolver;
import io.micronaut.http.client.HttpVersionSelection;
import io.micronaut.http.ssl.SslConfiguration;
import io.netty.handler.ssl.SslContextBuilder;
import jakarta.inject.Singleton;

@Requires(env = Environment.TEST)
@Singleton
@BootstrapContextCompatible
@Secondary
@Replaces(NettyClientSslBuilder.class)
class NettyClientSslBuilderReplacement extends NettyClientSslBuilder {
    NettyClientSslBuilderReplacement(ResourceResolver resourceResolver) {
        super(resourceResolver);
    }

    @Override
    protected SslContextBuilder createSslContextBuilder(SslConfiguration ssl, HttpVersionSelection versionSelection) {
        SslContextBuilder builder = super.createSslContextBuilder(ssl, versionSelection);
        builder.endpointIdentificationAlgorithm(null);
        return builder;
    }
}
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.

2.1.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:

implementation("io.micronaut:micronaut-http-client-jdk")
Note
This implementation of the Micronaut HTTP Client is available since Micronaut Framework 4.0.

The implementation based on Java HTTP Client does not support the following features:

  • Proxy request support (we do support HTTP Proxies).

  • Client Filters.

  • Streaming support.

  • Multipart requests.

  • H2C and HTTP/3.

  • pcap logging

If you require any of these, we recommend you use the implementation of the HTTP Client based on Netty.

2.2 Low-Level and High-Level APIs

Since the higher level API is built on the low-level HTTP client, we first introduce the low-level client.

2.3 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 Reactor Flux types.

Tip
See the guide for Micronaut HTTP Client to learn more.

2.3.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:

Injecting an HTTP client
@Client("https://api.twitter.com/1.1") @Inject HttpClient httpClient;

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:

  • An absolute URI, e.g. https://api.twitter.com/1.1

  • 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);

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:

logback.xml
<logger name="io.micronaut.http.client" level="TRACE"/>

Client Specific Debugging / Tracing

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):

micronaut.http.client.logger-name=mylogger
micronaut.http.services.otherClient.logger-name=other.client

Then enable logging in logback.xml:

logback.xml
<logger name="mylogger" level="DEBUG"/>
<logger name="other.client" level="TRACE"/>

Customizing the HTTP Request

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:

Passing an HttpRequest to retrieve
Flux<String> response = Flux.from(client.retrieve(
        GET("/hello/John")
        .header("X-My-Header", "SomeValue")
));

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);
}

The method above returns a POJO of type Message which looks like:

Message POJO
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

public class Message {

    private final String text;

    @JsonCreator
    public Message(@JsonProperty("text") String text) {
        this.text = text;
    }

    public String getText() {
        return text;
    }
}
Note
Jackson annotations are used to map the constructor

On the client you can call this endpoint and decode the JSON into a map using the retrieve method as follows:

Decoding the response body to a Map
Flux<Map> response = Flux.from(client.retrieve(
        GET("/greet/John"), Map.class
));

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:

Decoding the response body to a POJO
Flux<Message> response = Flux.from(client.retrieve(
        GET("/greet/John"), Message.class
));

assertEquals("Hello John", response.blockFirst().getText());

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.

2.3.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:

Table 1. HttpRequest Factory Methods
Method Description Allows Body

HttpRequest.GET(java.lang.String)

Constructs an HTTP GET request

false

HttpRequest.OPTIONS(java.lang.String)

Constructs an HTTP OPTIONS request

false

HttpRequest.HEAD(java.lang.String)

Constructs an HTTP HEAD request

false

HttpRequest.POST(java.lang.String,T)

Constructs an HTTP POST request

true

HttpRequest.PUT(java.lang.String,T)

Constructs an HTTP PUT request

true

HttpRequest.PATCH(java.lang.String,T)

Constructs an HTTP PATCH request

true

HttpRequest.DELETE(java.lang.String)

Constructs an HTTP DELETE request

true

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:

Resulting JSON
{"text":"Hello John"}

The JSON can be customized using Jackson Annotations.

Using a URI Template

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
));

In the above case the title property is included in the URI.

Sending Form Data

You can also encode a POJO or a map as form data instead of JSON. Just set the content type to application/x-www-form-urlencoded on the post request:

Sending a Form Data
Flux<HttpResponse<Book>> call = Flux.from(client.exchange(
        POST("/amazon/book/{title}", new Book("The Stand"))
        .contentType(MediaType.APPLICATION_FORM_URLENCODED),
        Book.class
));

Note that Jackson can bind form data too, so to customize the binding process use Jackson annotations.

2.3.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

2.3.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:

Table 1. HTTP Streaming Methods
Method Description

dataStream(HttpRequest<I> request)

Returns a stream of data as a Flux of ByteBuffer

exchangeStream(HttpRequest<I> request)

Returns the HttpResponse wrapping a Flux of ByteBuffer

jsonStream(HttpRequest<I> request)

Returns a non-blocking stream of JSON objects

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.

2.3.5 Configuring HTTP clients

Global Configuration for All Clients

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):

Altering default HTTP client configuration
micronaut.http.client.read-timeout=5s

The above example sets the readTimeout property of the HttpClientConfiguration class.

Client Specific Configuration

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:

Manually configuring HTTP services
micronaut.http.services.foo.urls[0]=http://foo1
micronaut.http.services.foo.urls[1]=http://foo2
micronaut.http.services.foo.read-timeout=5s
  • The read-timeout is applied to the foo client.

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.

Then, inject the named client configuration:

Injecting an HTTP client
@Client("foo") @Inject ReactorHttpClient httpClient;

You can also define a bean that extends from HttpClientConfiguration and ensure that the jakarta.inject.Named annotation names it appropriately:

Defining an HTTP client configuration bean
@Named("twitter")
@Singleton
class TwitterHttpClientConfiguration extends HttpClientConfiguration {
   public TwitterHttpClientConfiguration(ApplicationConfiguration configuration) {
        super(configuration);
    }
}

This configuration will be picked up if you inject a service named twitter with @Client using Service Discovery:

Injecting an HTTP client
@Client("twitter") @Inject ReactorHttpClient httpClient;

Alternatively, if you don’t use service discovery you can use the configuration member of @Client to refer to a specific type:

Injecting an HTTP client
@Client(value = "https://api.twitter.com/1.1",
        configuration = TwitterHttpClientConfiguration.class)
@Inject
ReactorHttpClient httpClient;

Connection Pooling and HTTP/2

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.

Manually configuring HTTP services
micronaut.http.services.foo.urls[0]=http://foo1
micronaut.http.services.foo.urls[1]=http://foo2
micronaut.http.services.foo.pool.max-concurrent-http1-connections=50
micronaut.http.services.foo.pool.enabled=true
micronaut.http.services.foo.pool.max-connections=50
  • Limit maximum concurrent HTTP/1.1 connections

  • max-concurrent-http1-connections limits the maximum concurrent HTTP/1.1 connections to 50.

  • pool enables the pool and sets the maximum number of connections for it

See the API for ConnectionPoolConfiguration for details on available pool configuration options.

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:

Configuring The Default Event Loop
micronaut.netty.event-loops.default.num-threads=10

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:

Configuring Additional Event Loops
micronaut.netty.event-loops.other.num-threads=10

Once an additional event loop has been configured you can alter the HTTP client configuration to use it:

Altering the Event Loop Group used by Clients
micronaut.http.client.event-loop-group=other

2.3.6 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 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.

2.3.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"));
    }
}

2.4 Proxying Requests with ProxyHttpClient

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.

2.5 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;
    }
}

You can define a common interface for saving new Pet instances:

PetOperations.java
import io.micronaut.http.annotation.Post;
import io.micronaut.validation.Validated;
import org.reactivestreams.Publisher;
import io.micronaut.core.async.annotation.SingleResult;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;

@Validated
public interface PetOperations {
    @Post
    @SingleResult
    Publisher<Pet> save(@NotBlank String name, @Min(1L) int 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:

implementation("io.micronaut:micronaut-validation")

On the server-side of the Micronaut framework you can implement the PetOperations interface:

PetController.java
import io.micronaut.http.annotation.Controller;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import io.micronaut.core.async.annotation.SingleResult;

@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);
    }
}

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.

Recall that the value of @Client can be:

  • An absolute URI, e.g. https://api.twitter.com/1.1

  • 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:

Table 1. Micronaut Response Types
Type Description Example Signature

Publisher

Any type that implements the Publisher interface

Flux<String> hello()

HttpResponse

An HttpResponse and optional response body type

Mono<HttpResponse<String>> hello()

Publisher

A Publisher implementation that emits a POJO

Mono<Book> hello()

CompletableFuture

A Java CompletableFuture instance

CompletableFuture<String> hello()

CharSequence

A blocking native type. Such as String

String hello()

T

Any simple POJO type.

Book show()

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.

2.5.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
@SingleResult
Publisher<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:

Table 1. Parameter Binding Annotations
Annotation Description Example

@Body

Specifies the parameter for the body of the request

@Body String body

@CookieValue

Specifies parameters to be sent as cookies

@CookieValue String myCookie

@Header

Specifies parameters to be sent as HTTP headers

@Header String requestId

@QueryValue

Customizes the name of the URI parameter to bind from

@QueryValue("userAge") Integer age

@PathVariable

Binds a parameter exclusively from a Path Variable.

@PathVariable Long id

@RequestAttribute

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&param=Adam&param=Kate"

"name=Mike&age=30"

"DEEP_OBJECT"

"param[0]=Mike&param[1]=Adam&param[2]=Kate"

"param[name]=Mike&param[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:

Type Description Example

BasicAuth

Sets the Authorization header

BasicAuth basicAuth

HttpHeaders

Adds multiple headers to the request

HttpHeaders headers

Cookies

Adds multiple cookies to the request

Cookies cookies

Cookie

Adds a cookie to the request

Cookie cookie

Locale

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.

In this example, see the following client:

Client With @Metadata Argument
@Client("/")
public interface MetadataClient {

    @Get("/client/bind")
    String get(@Metadata Map<String, Object> metadata);
}

The argument is annotated with the following annotation:

@Metadata Annotation
import io.micronaut.core.bind.annotation.Bindable;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Documented
@Retention(RUNTIME)
@Target(PARAMETER)
@Bindable
public @interface Metadata {
}

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.

Annotation Argument Binder
import org.jspecify.annotations.NonNull;
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.naming.NameUtils;
import io.micronaut.core.util.StringUtils;
import io.micronaut.http.MutableHttpRequest;
import io.micronaut.http.client.bind.AnnotatedClientArgumentRequestBinder;
import io.micronaut.http.client.bind.ClientRequestUriContext;

import jakarta.inject.Singleton;
import java.util.Map;

@Singleton
public class MetadataClientArgumentBinder implements AnnotatedClientArgumentRequestBinder<Metadata> {

    @NonNull
    @Override
    public Class<Metadata> getAnnotationType() {
        return Metadata.class;
    }

    @Override
    public void bind(@NonNull ArgumentConversionContext<Object> context,
                     @NonNull ClientRequestUriContext uriContext,
                     @NonNull Object value,
                     @NonNull MutableHttpRequest<?> request) {
        if (value instanceof Map<?,?> map) {
            for (Map.Entry<?, ?> entry: map.entrySet()) {
                String key = NameUtils.hyphenate(StringUtils.capitalize(entry.getKey().toString()), false);
                request.header("X-Metadata-" + key, entry.getValue().toString());
            }
        }
    }
}
Binding By Type

To bind to the request based on the type of the argument, create a bean of type TypedClientArgumentRequestBinder.

In this example, see the following client:

Client With Metadata Argument
@Client("/")
public interface MetadataClient {

    @Get("/client/bind")
    String get(Metadata metadata);
}

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.

Typed Argument Binder
import org.jspecify.annotations.NonNull;
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.type.Argument;
import io.micronaut.http.MutableHttpRequest;
import io.micronaut.http.client.bind.ClientRequestUriContext;
import io.micronaut.http.client.bind.TypedClientArgumentRequestBinder;

import jakarta.inject.Singleton;

@Singleton
public class MetadataClientArgumentBinder implements TypedClientArgumentRequestBinder<Metadata> {

    @Override
    @NonNull
    public Argument<Metadata> argumentType() {
        return Argument.of(Metadata.class);
    }

    @Override
    public void bind(@NonNull ArgumentConversionContext<Metadata> context,
                     @NonNull ClientRequestUriContext uriContext,
                     @NonNull Metadata value,
                     @NonNull MutableHttpRequest<?> request) {
        request.header("X-Metadata-Version", value.getVersion().toString());
        request.header("X-Metadata-Deployment-Id", value.getDeploymentId().toString());
    }
}

Binding On Method

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

2.5.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 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:

Adjusting the readIdleTimeout
micronaut.http.client.read-idle-timeout=10m

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:

SSE Client
@Client("/streaming/sse")
public interface HeadlineClient {

    @Get(value = "/headlines", processes = TEXT_EVENT_STREAM)
    Publisher<Event<Headline>> streamHeadlines();
}

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.

2.5.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.

2.5.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 following example serves to illustrate this:

Defining Headers via Configuration
@Client("/pets")
@Header(name="X-Pet-Client", value="${pet.client.id}")
public interface PetClient extends PetOperations {

    @Override
    @SingleResult
    Publisher<Pet> save(String name, int age);

    @Get("/{name}")
    @SingleResult
    Publisher<Pet> get(String name);
}

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

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.

2.5.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:

Example Jackson Configuration
jackson.serialization.indentOutput=true

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):

Example of JacksonFeatures
@Client(id = EurekaClient.SERVICE_ID,
        path = "/eureka",
        configuration = EurekaConfiguration.class)
@JacksonFeatures(
    enabledSerializationFeatures = WRAP_ROOT_VALUE,
    disabledSerializationFeatures = WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED,
    enabledDeserializationFeatures = {UNWRAP_ROOT_VALUE, ACCEPT_SINGLE_VALUE_AS_ARRAY}
)
public interface EurekaClient {
    ...
}

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.

2.5.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.
implementation("io.micronaut:micronaut-retry")

You can declare the @Retryable or @CircuitBreaker annotations on any @Client interface and the retry policy will be applied, for example:

Declaring @Retryable
@Client("/pets")
@Retryable
public interface PetClient extends PetOperations {

    @Override
    @SingleResult
    Publisher<Pet> save(String name, int age);
}

For more information on customizing retry, see the section on Retry Advice.

2.5.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
@Fallback
public 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);
    }
}
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.

2.5.8 Netflix Hystrix Support

Tip
Using the CLI

If you create your project using the Micronaut CLI, supply the netflix-hystrix feature to configure Hystrix in your project:

$ mn create-app my-app --features netflix-hystrix

Netflix Hystrix is a fault tolerance library developed by the Netflix team and is designed to improve resilience of interprocess communication.

The Micronaut framework integrates with Hystrix through the netflix-hystrix module, which you can add to your build:

implementation("io.micronaut.netflix:micronaut-netflix-hystrix")

Using the @HystrixCommand Annotation

With the above dependency declared you can annotate any method (including methods defined on @Client interfaces) with the HystrixCommand annotation, and method’s execution will be wrapped in a Hystrix command. For example:

Using @HystrixCommand
@HystrixCommand
String hello(String name) {
    return "Hello $name"
}
Note
This works for reactive return types such as Flux, and the reactive type will be wrapped in a HystrixObservableCommand.

The HystrixCommand annotation also integrates with Micronaut’s support for Retry Advice and Fallbacks

Tip
For information on how to customize the Hystrix thread pool, group, and properties, see the Javadoc for HystrixCommand.

Enabling Hystrix Stream and Dashboard

You can enable a Server Sent Event stream to feed into the Hystrix Dashboard by setting the hystrix.stream.enabled setting to true in your configuration file (e.g application.yml):

Enabling Hystrix Stream
hystrix.stream.enabled=true

This exposes a /hystrix.stream endpoint with the format the Hystrix Dashboard expects.

2.6 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.

The following example which implements a filter allowing authentication between services on Google Cloud Run demonstrates how to use BeanProvider to inject another client:

Filter Matching By Annotation

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.

2.7 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:

Enabling HTTP/2 in Clients
micronaut.http.client.http-version=2

Or by specifying the HTTP version to use when injecting the client:

Injecting an HTTP/2 Client
@Inject
@Client(httpVersion=HttpVersion.HTTP_2_0)
ReactorHttpClient client;

2.8 HTTP/3 Support

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.

Enabling HTTP/3 in Clients
micronaut:
  http:
    client:
      alpn-modes: [h3]

Additionally, the netty HTTP/3 codec needs to be present on the classpath:

implementation("io.micronaut:micronaut-http-netty-http3")

2.9 HTTP Client Sample

Tip
Read the HTTP Client Guide (Java, Groovy, Kotlin), a step-by-step tutorial, to learn more.

2.10 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.*. 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.

3 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.server.ssl.key-name / micronaut.server.ssl.trust-name

  • micronaut.http.client.ssl.key-name / micronaut.http.client.ssl.trust-name

Micronaut ships three provider types: - File-based: load from files on disk with optional auto-reload - Resource-based: load from Micronaut resources (classpath:, file:, string:, base64:) - Self-signed: generate ephemeral certificates (testing/dev)

File-based provider (PEM with separate key and cert, auto-refresh)

The file provider loads a private key and certificate chain from disk and can automatically reload when files change.

micronaut.ssl.enabled=true
micronaut.server.ssl.key-name=cert-a
micronaut.server.ssl.key.password=
micronaut.http.client.ssl.trust-name=cert-a
micronaut.certificate.file.cert-a.format=pem
micronaut.certificate.file.cert-a.path=/path/to/server.key.pem
micronaut.certificate.file.cert-a.certificate-path=/path/to/server.crt.pem

Resource-based provider (inline PEM and cert-only trust store)

The resource provider loads the material from any Micronaut resource location.

micronaut.ssl.enabled=true
micronaut.server.ssl.key-name=cert-a
micronaut.http.client.ssl.trust-name=trust-a
micronaut.certificate.resource.cert-a.resource=string:-----BEGIN PRIVATE KEY-----
...base64...
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
...base64...
-----END CERTIFICATE-----

micronaut.certificate.resource.trust-a.resource=string:-----BEGIN CERTIFICATE-----
...base64...
-----END CERTIFICATE-----

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:

runtimeOnly("io.netty:netty-pkitesting")
micronaut.ssl.enabled=true
micronaut.server.ssl.key-name=cert-server
micronaut.server.ssl.trust-name=cert-client
micronaut.server.ssl.client-authentication=need
micronaut.http.client.ssl.key-name=cert-client
micronaut.http.client.ssl.trust-name=cert-server
micronaut.certificate.self-signed.cert-client.subject=CN=foo

For per-listener SSL in Netty server, you can also set key-name and trust-name under micronaut.server.netty.listeners.<listener>.ssl.