Micronaut Servlet

Provides integration between Micronaut and the Servlet API

Version: 6.2.0-SNAPSHOT

1 Introduction

This project implements a Micronaut HTTP server backed onto the Servlet API and includes various subprojects that allow running popular Servlet containers as servers.

This project is for users who fall into one of the following categories:

  • Users who want to use Micronaut but the target deployment environment is based on Servlets

  • Users who prefer the thread per connection model of the Servlet API over the Event Loop model provided by the default Netty-based HTTP server

  • Users who have existing Servlets and/or Filters that they wish to combine with Micronaut.

2 Release History

For this project, you can find a list of releases (with release notes) here:

3 Working with the Servlet API

In general, you can follow the documentation for the HTTP server when building applications. All non-Netty specific features of the default HTTP server should work the same for Servlet containers (Report an issue if you find a difference).

There are a couple of additional extensions within Micronaut Servlet that make it easier to work with the Servlet API which are detailed in the following sections.

Threading Model

On a container that supports asynchronous servlets (Jetty, Tomcat and Undertow) a request is handled as follows:

  • A request without a body, or with a body that declares a Content-Length no larger than micronaut.server.max-request-buffer-size (10MB by default) and is not a form submission, is handled entirely on the container thread that received it. The body, if any, is read on that thread before the route runs, as a blocking servlet application would, and the response is written from the same thread unless the route returns a reactive or asynchronous type.

  • A request with a larger body or a chunked body is dispatched through AsyncContext.start and its body is read through a ReadListener as it arrives, so no container thread waits for it.

  • A form or multipart body is dispatched the same way, but its fields are not streamed: the container parses them itself when the route asks for its parameters or parts. A URL-encoded form without a declared length is the one exception, which the servlet runtime reads through micronaut.server.max-request-size and decodes itself, since the container would otherwise parse it unbounded.

Reactive and asynchronous return types complete the response from whichever thread produces the result, and @ExecuteOn moves the route to the named executor as on the Netty server. When micronaut.servlet.async-supported is false, or on the built-in JDK server, every request is handled on the container thread and its body is read from a blocking stream.

Injecting the Servlet Request and Response

You can receive the HttpServletRequest and HttpServletResponse objects directly as parameters:

Using the Request and Response
@Get("/hello")
void process(
        HttpServletRequest request, (1)
        HttpServletResponse response) (2)
        throws IOException {
    response.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN);
    response.setStatus(HttpStatus.ACCEPTED.getCode());
    try (final PrintWriter writer = response.getWriter()) {
        writer.append("Hello ").append(request.getParameter("name"));
        writer.flush();
    }
}
1 The request object
2 The response object

Simplified I/O code with Readable and Writable

Writing to the response and reading from the request can be simplified with Micronaut’s Readable and Writable interfaces:

Using Readable and Writable
import io.micronaut.core.io.Readable;
import io.micronaut.core.io.Writable;

@Post(value = "/writable", processes = "text/plain")
Writable readAndWrite(@Body Readable readable) throws IOException {
    return out -> {
        try (BufferedReader reader = new BufferedReader(readable.asReader())) {
            out.append("Hello ").append(reader.readLine());
        }
    };
}

Multipart support with @Part

Multipart support is improved with the ability to inject parts using the annotation io.micronaut.http.annotation.Part. For example:

Using @Part
@Post(value = "/multipart", consumes = MediaType.MULTIPART_FORM_DATA, produces = "text/plain")
String multipart(
        String attribute, (1)
        @Part("one") Person person, (2)
        @Part("two") String text, (3)
        @Part("three") byte[] bytes, (4)
        @Part("four") jakarta.servlet.http.Part raw, (5)
        @Part("five") CompletedPart part) { (6)
    return "Ok";
}
1 You can receive attributes with just parameter names that match the attribute name
2 Parts that have a content type of application/json can be bound to POJOs
3 You can read parts as text
4 You can read parts as byte[]
5 You can receive the raw jakarta.servlet.http.Part
6 You can receive Micronaut’s CompletedPart interface which works with Netty too

4 Servlet Annotation Support

To use the Servlet APIs annotations to register servlets, filters and listeners you first need to add the following annotation processor dependency:

annotationProcessor("io.micronaut.servlet:micronaut-servlet-processor")
<annotationProcessorPaths>
    <path>
        <groupId>io.micronaut.servlet</groupId>
        <artifactId>micronaut-servlet-processor</artifactId>
    </path>
</annotationProcessorPaths>

The following annotations can then be used from the Servlet API to add additional servlets, filters and listeners as beans:

  • @WebFilter - Applicable to types that implement the jakarta.servlet.Filter interface.

  • @WebServlet - Applicable to types that implement the jakarta.servlet.Servlet interface.

  • @WebListener - See the annotation javadoc for applicable types.

The @Order annotation can be used to control registration order and hence filter order. For example a value of io.micronaut.core.order.Ordered.HIGHEST_PRECEDENCE will run the filter first.

Using HIGHEST_PRECEDENCE will prevent any other filter running before your filter. The default position is 0 and HIGHEST_PRECEDENCE == Integer.MIN_VALUE hence you should consider using constants to the represent the position of your filter that exist somewhere between HIGHEST_PRECEDENCE and LOWEST_PRECEDENCE.

In addition, you can use the following annotations on methods of @Factory beans to instantiate servlets and filters and register them:

The following example adds a new Servlet filter with the highest precedence:

Adding a Filter with a Factory
import io.micronaut.context.annotation.Factory;
import io.micronaut.core.annotation.Order;
import io.micronaut.core.order.Ordered;
import io.micronaut.servlet.api.annotation.ServletFilterBean;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.GenericFilter;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import java.io.IOException;

@Factory // (1)
public class MyFilterFactory {

    @ServletFilterBean(
        filterName = "another", // (2)
        value = {"/extra-filter/*", "${my.filter.mapping}"}) // (3)
    @Order(Ordered.HIGHEST_PRECEDENCE) // (4)
    Filter myOtherFilter() {
        return new GenericFilter() {
            @Override
            public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
                request.setAttribute("runFirst", true);
                chain.doFilter(request, response);
            }
        };
    }
}
1 A @Factory bean is defined
2 The @ServletFilterBean annotation is used and a filter name defined
3 1 or more mappings are defined. Note these can be resolved from property placeholder configuration if necessary.
4 The order of the filter is defined.
Servlet Filters are not to be confused with Micronaut filters. Servlet Filters always run before the Micronaut Servlet which in turn runs the Micronaut Filters hence it is not possible to place a Servlet Filter after Micronaut Filters.

The same processor maps the Jakarta WebSocket annotations - @ServerEndpoint, @ClientEndpoint, @OnOpen, @OnMessage, @OnClose, @OnError and @PathParam - onto Micronaut’s WebSocket support. See the WebSocket section.

5 WebSocket Support

Micronaut’s WebSocket programming model works on Jetty, Tomcat and Undertow. The @ServerWebSocket beans, the WebSocketSession, the WebSocketBroadcaster and the argument binding of @OnOpen, @OnMessage, @OnClose and @OnError behave the same as they do on the Netty server.

To enable it, add the WebSocket module:

implementation("io.micronaut.servlet:micronaut-servlet-websocket")
<dependency>
    <groupId>io.micronaut.servlet</groupId>
    <artifactId>micronaut-servlet-websocket</artifactId>
</dependency>

and the Jakarta WebSocket implementation of the container you are using, which is a separate artifact in all three cases:

Jetty

runtimeOnly("org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-server")
<dependency>
    <groupId>org.eclipse.jetty.ee10.websocket</groupId>
    <artifactId>jetty-ee10-websocket-jakarta-server</artifactId>
    <scope>runtime</scope>
</dependency>

Tomcat

runtimeOnly("org.apache.tomcat.embed:tomcat-embed-websocket")
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-websocket</artifactId>
    <scope>runtime</scope>
</dependency>

Undertow

runtimeOnly("io.undertow:undertow-websockets-jsr")
<dependency>
    <groupId>io.undertow</groupId>
    <artifactId>undertow-websockets-jsr</artifactId>
    <scope>runtime</scope>
</dependency>

The Jetty artifact pulls in jetty-ee10-annotations, which only Jetty’s WebAppContext configuration uses and which the embedded server never loads. You can exclude it:

runtimeOnly("org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-server") {
    exclude group: "org.eclipse.jetty.ee10", module: "jetty-ee10-annotations"
}

An endpoint is written exactly as it is on the Netty server:

@ServerWebSocket("/ws/chat/{topic}/{username}") // (1)
public class ChatServerWebSocket {

    private final WebSocketBroadcaster broadcaster;

    public ChatServerWebSocket(WebSocketBroadcaster broadcaster) {
        this.broadcaster = broadcaster;
    }

    @OnOpen // (2)
    public void onOpen(String topic, String username, WebSocketSession session) {
        broadcaster.broadcastSync("[" + username + "] Joined!", isValid(topic, session));
    }

    @OnMessage // (3)
    public void onMessage(String topic, String username, String message, WebSocketSession session) {
        broadcaster.broadcastSync("[" + username + "] " + message, isValid(topic, session)); // (4)
    }

    @OnClose // (5)
    public void onClose(String topic, String username, WebSocketSession session) {
        broadcaster.broadcastSync("[" + username + "] Disconnected!", isValid(topic, session));
    }

    private Predicate<WebSocketSession> isValid(String topic, WebSocketSession session) {
        return s -> s != session && topic.equalsIgnoreCase(s.getUriVariables().get("topic", String.class, null));
    }
}
@ServerWebSocket("/ws/chat/{topic}/{username}") // (1)
class ChatServerWebSocket(private val broadcaster: WebSocketBroadcaster) {

    @OnOpen // (2)
    fun onOpen(topic: String, username: String, session: WebSocketSession) {
        broadcaster.broadcastSync("[$username] Joined!", isValid(topic, session))
    }

    @OnMessage // (3)
    fun onMessage(topic: String, username: String, message: String, session: WebSocketSession) {
        broadcaster.broadcastSync("[$username] $message", isValid(topic, session)) // (4)
    }

    @OnClose // (5)
    fun onClose(topic: String, username: String, session: WebSocketSession) {
        broadcaster.broadcastSync("[$username] Disconnected!", isValid(topic, session))
    }

    private fun isValid(topic: String, session: WebSocketSession) = Predicate<WebSocketSession> {
        it !== session && topic.equals(it.uriVariables.get("topic", String::class.java).orElse(null), ignoreCase = true)
    }
}
@ServerWebSocket("/ws/chat/{topic}/{username}") // (1)
class ChatServerWebSocket {

    private final WebSocketBroadcaster broadcaster

    ChatServerWebSocket(WebSocketBroadcaster broadcaster) {
        this.broadcaster = broadcaster
    }

    @OnOpen // (2)
    void onOpen(String topic, String username, WebSocketSession session) {
        broadcaster.broadcastSync("[" + username + "] Joined!", isValid(topic, session))
    }

    @OnMessage // (3)
    void onMessage(String topic, String username, String message, WebSocketSession session) {
        broadcaster.broadcastSync("[" + username + "] " + message, isValid(topic, session)) // (4)
    }

    @OnClose // (5)
    void onClose(String topic, String username, WebSocketSession session) {
        broadcaster.broadcastSync("[" + username + "] Disconnected!", isValid(topic, session))
    }

    private Predicate<WebSocketSession> isValid(String topic, WebSocketSession session) {
        return { WebSocketSession s ->
            s !== session && topic.equalsIgnoreCase(s.getUriVariables().get("topic", String, null))
        }
    }
}
1 The @ServerWebSocket annotation defines the URI the endpoint is mapped to, including any path variables.
2 @OnOpen runs once the handshake has completed. Its arguments are bound from the path variables, the originating request and the session.
3 @OnMessage receives each message. The single argument that cannot be bound from anywhere else is the message body.
4 WebSocketBroadcaster sends a message to every open session that matches the given predicate.
5 @OnClose runs when the connection is closed, whichever side closed it.

Clients are written with @ClientWebSocket and are unchanged from the Netty server:

@ClientWebSocket("/ws/chat/{topic}/{username}") // (1)
public abstract class ChatClientWebSocket implements AutoCloseable { // (2)

    private final Collection<String> replies = new ConcurrentLinkedQueue<>();

    @OnMessage
    public void onMessage(String message) {
        replies.add(message); // (3)
    }

    public abstract void send(String message); // (4)

    public Collection<String> getReplies() {
        return replies;
    }
}
@ClientWebSocket("/ws/chat/{topic}/{username}") // (1)
abstract class ChatClientWebSocket : AutoCloseable { // (2)

    val replies: MutableCollection<String> = ConcurrentLinkedQueue()

    @OnMessage
    fun onMessage(message: String) {
        replies.add(message) // (3)
    }

    abstract fun send(message: String) // (4)
}
@ClientWebSocket("/ws/chat/{topic}/{username}") // (1)
abstract class ChatClientWebSocket implements AutoCloseable { // (2)

    private final Collection<String> replies = new ConcurrentLinkedQueue<>()

    @OnMessage
    void onMessage(String message) {
        replies.add(message) // (3)
    }

    abstract void send(String message) // (4)

    Collection<String> getReplies() {
        return replies
    }
}
1 The client endpoint is mapped to the same URI template.
2 The class is abstract so that Micronaut can implement the send methods.
3 @OnMessage receives messages sent by the server.
4 Any abstract method whose name begins with send or broadcast is implemented to send a message over the session.

Jakarta WebSocket annotations

An endpoint can also be written with the Jakarta WebSocket annotations, @ServerEndpoint, @OnOpen, @OnMessage, @OnClose, @OnError and @PathParam, in the same way that @WebServlet and @WebFilter can be used to register servlets and filters. This needs the annotation processor:

annotationProcessor("io.micronaut.servlet:micronaut-servlet-processor")
<annotationProcessorPaths>
    <path>
        <groupId>io.micronaut.servlet</groupId>
        <artifactId>micronaut-servlet-processor</artifactId>
    </path>
</annotationProcessorPaths>

The processor maps the Jakarta annotations onto their Micronaut equivalents at compilation time, so the endpoint is a @ServerWebSocket bean and takes the same path as one: the handshake goes through the Micronaut router and filter chain, and the handlers are invoked through the compiled bean metadata. The container never scans the class and nothing is reflected on, so a Jakarta endpoint works in a native image as any Micronaut bean does.

import jakarta.websocket.CloseReason;
import jakarta.websocket.OnClose;
import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;

import java.io.IOException;

@ServerEndpoint("/ws/echo/{room}") // (1)
public class EchoEndpoint {

    private String room;

    @OnOpen
    public void onOpen(Session session, @PathParam("room") String room) throws IOException { // (2)
        this.room = room;
        session.getBasicRemote().sendText("Welcome to " + room); // (3)
    }

    @OnMessage
    public String onMessage(String message) { // (4)
        return "[" + room + "] " + message;
    }

    @OnClose
    public void onClose(CloseReason reason) { // (5)
    }
}
import jakarta.websocket.CloseReason
import jakarta.websocket.OnClose
import jakarta.websocket.OnMessage
import jakarta.websocket.OnOpen
import jakarta.websocket.Session
import jakarta.websocket.server.PathParam
import jakarta.websocket.server.ServerEndpoint

@ServerEndpoint("/ws/echo/{room}") // (1)
class EchoEndpoint {

    private lateinit var room: String

    @OnOpen
    fun onOpen(session: Session, @PathParam("room") room: String) { // (2)
        this.room = room
        session.basicRemote.sendText("Welcome to $room") // (3)
    }

    @OnMessage
    fun onMessage(message: String): String { // (4)
        return "[$room] $message"
    }

    @OnClose
    fun onClose(reason: CloseReason) { // (5)
    }
}
import jakarta.websocket.CloseReason
import jakarta.websocket.OnClose
import jakarta.websocket.OnMessage
import jakarta.websocket.OnOpen
import jakarta.websocket.Session
import jakarta.websocket.server.PathParam
import jakarta.websocket.server.ServerEndpoint

@ServerEndpoint("/ws/echo/{room}") // (1)
class EchoEndpoint {

    private String room

    @OnOpen
    void onOpen(Session session, @PathParam("room") String room) { // (2)
        this.room = room
        session.basicRemote.sendText("Welcome to " + room) // (3)
    }

    @OnMessage
    String onMessage(String message) { // (4)
        "[" + room + "] " + message
    }

    @OnClose
    void onClose(CloseReason reason) { // (5)
    }
}
1 @ServerEndpoint maps the endpoint. The URI template syntax is the same as @ServerWebSocket’s, and `subprotocols is honoured.
2 A handler can take the container’s jakarta.websocket.Session and EndpointConfig by type, @PathParam binds a path variable under the name it declares, and everything a Micronaut handler can take - the WebSocketSession, @Header, @QueryValue, the HttpRequest - can be taken too.
3 The Session is the container’s own, so getBasicRemote(), getAsyncRemote(), getOpenSessions() and getUserProperties() all work as they do under the container.
4 A value returned by @OnMessage is sent to the peer, as the specification asks. An endpoint may declare one text, one binary and one pong @OnMessage method, chosen by the type of the message parameter.
5 @OnClose may take the Jakarta CloseReason, @OnError the Throwable.

Unless the class declares a scope of its own, one endpoint instance is created per connection, which is the Jakarta contract and lets the endpoint keep the session and its state in fields. An explicit @Singleton shares one instance between all connections.

The processor reports at compilation time what could not work at runtime: a server endpoint without an @OnMessage method, more than one handler for a message category, a partial message handler (the boolean last-part parameter, since messages are always delivered whole), and a maxMessageSize beyond Integer.MAX_VALUE.

Decoders, encoders and the configurator

decoders, encoders and configurator name classes the container would instantiate with Class.newInstance(). Here the annotation processor generates an introspection for every class the endpoint names, so a class with a public no-argument constructor - the Jakarta contract - is instantiated through that, without reflection, and needs nothing else. A class is resolved through the first of these that can produce it:

  1. As a bean, when the class carries a scope such as @Singleton or @Prototype. The bean can inject an ObjectMapper or anything else, and its scope decides whether it is shared.

  2. Through its generated introspection, when it has a no-argument constructor.

  3. Reflectively, when io.micronaut:micronaut-reflection is on the classpath and the type matches a micronaut.introspection.allow-reflection pattern. That is the same switch that lets the shared introspector describe a type reflectively, so there is one place to opt a package into reflection. This is only needed for a class whose constructor takes arguments without being a bean; they are then injected.

    micronaut.introspection.allow-reflection[0]=com.example.legacy.codecs.*
    micronaut:
      introspection:
        allow-reflection:
          - com.example.legacy.codecs.*
    micronaut = {introspection = {allow-reflection = ["com.example.legacy.codecs.*"]}}
    micronaut {
      introspection {
        allowReflection = ["com.example.legacy.codecs.*"]
      }
    }
    {
      micronaut {
        introspection {
          allow-reflection = ["com.example.legacy.codecs.*"]
        }
      }
    }
    {
      "micronaut": {
        "introspection": {
          "allow-reflection": ["com.example.legacy.codecs.*"]
        }
      }
    }

A class none of these can produce - one whose constructor takes arguments, when micronaut-reflection is not on the compile classpath - is reported at compilation time.

Decoders run before Micronaut’s own conversion and message body readers, and encoders apply to the value an @OnMessage method returns, before Micronaut’s message body writers. Either can be left out: a message type no decoder produces is read by the MessageBodyReader for the media type the handler @Consumes (JSON by default), and a return value no encoder accepts is written by the MessageBodyWriter for the media type it @Produces (JSON by default), exactly as for a Micronaut endpoint. The type an encoder handles is read from its bean definition, or from its generic signature when micronaut-reflection is present; an encoder whose type is unknown is offered every value and stands aside for one it cannot encode.

RemoteEndpoint#sendObject on the raw session is served by the container, which instantiates the encoders itself, reflectively; the encoders are therefore only passed to the container for classes the reflection policy allows. Without that, return a value from the handler or send through the Micronaut WebSocketSession instead.

A declared configurator has its modifyHandshake, checkOrigin, getNegotiatedSubprotocol and getNegotiatedExtensions honoured; an origin must pass both its check and the Micronaut policy. Its getEndpointInstance is not called: the endpoint is always the bean.

Client endpoints

A @ClientEndpoint is supported the same way. The class is a plain bean whose handlers the processor makes executable, so unlike under a Jakarta container - which needs a public no-argument constructor - it can inject its dependencies through its constructor. It is opened through the jakarta.websocket.WebSocketContainer bean rather than ContainerProvider.getWebSocketContainer(), whose result depends on which implementation ServiceLoader happens to find first.

import jakarta.websocket.ClientEndpoint;
import jakarta.websocket.CloseReason;
import jakarta.websocket.OnClose;
import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;

import java.io.IOException;
import java.util.Collection;
import java.util.concurrent.ConcurrentLinkedQueue;

@ClientEndpoint // (1)
public class EchoClientEndpoint {

    private final Collection<String> replies = new ConcurrentLinkedQueue<>();
    private Session session;

    @OnOpen
    public void onOpen(Session session) { // (2)
        this.session = session;
    }

    @OnMessage
    public void onMessage(String message) { // (3)
        replies.add(message);
    }

    @OnClose
    public void onClose(CloseReason reason) {
        // the session is gone; nothing to release
    }

    public void send(String message) throws IOException {
        session.getBasicRemote().sendText(message); // (4)
    }

    public Collection<String> getReplies() {
        return replies;
    }
}
import jakarta.websocket.ClientEndpoint
import jakarta.websocket.CloseReason
import jakarta.websocket.OnClose
import jakarta.websocket.OnMessage
import jakarta.websocket.OnOpen
import jakarta.websocket.Session
import java.util.concurrent.ConcurrentLinkedQueue

@ClientEndpoint // (1)
class EchoClientEndpoint {

    val replies: MutableCollection<String> = ConcurrentLinkedQueue()
    private lateinit var session: Session

    @OnOpen
    fun onOpen(session: Session) { // (2)
        this.session = session
    }

    @OnMessage
    fun onMessage(message: String) { // (3)
        replies.add(message)
    }

    @OnClose
    fun onClose(reason: CloseReason) {
        // the session is gone; nothing to release
    }

    fun send(message: String) {
        session.basicRemote.sendText(message) // (4)
    }
}
import jakarta.websocket.ClientEndpoint
import jakarta.websocket.CloseReason
import jakarta.websocket.OnClose
import jakarta.websocket.OnMessage
import jakarta.websocket.OnOpen
import jakarta.websocket.Session

import java.util.concurrent.ConcurrentLinkedQueue

@ClientEndpoint // (1)
class EchoClientEndpoint {

    final Collection<String> replies = new ConcurrentLinkedQueue<>()
    private Session session

    @OnOpen
    void onOpen(Session session) { // (2)
        this.session = session
    }

    @OnMessage
    void onMessage(String message) { // (3)
        replies.add(message)
    }

    @OnClose
    void onClose(CloseReason reason) {
        // the session is gone; nothing to release
    }

    void send(String message) {
        session.basicRemote.sendText(message) // (4)
    }
}
1 @ClientEndpoint takes subprotocols, decoders, encoders and configurator, which are honoured as for a @ServerEndpoint; the URI comes from connectToServer.
2 The handlers take what a server endpoint’s do: the container’s Session, the EndpointConfig, the Jakarta CloseReason, the Throwable, and anything a Micronaut handler binds, such as a @Header of the handshake request.
3 One text, one binary and one pong @OnMessage may be declared, and a value one returns is sent to the server. Unlike a server endpoint a client need not declare any: a client that only sends is valid.
4 Sending goes through the container’s session. The class’s own methods are its own, whatever their names.
WebSocketContainer container = embeddedServer.applicationContext.getBean(WebSocketContainer) // (1)
EchoClientEndpoint client = new EchoClientEndpoint()
Session session = container.connectToServer(client, URI.create("ws://localhost:${embeddedServer.port}/ws/echo/lobby")) // (2)
1 The container is a bean; inject it where the connection is opened.
2 connectToServer takes the endpoint class, which creates one instance per connection, or an instance. It returns once @OnOpen has completed, and throws a DeploymentException carrying the cause when it failed or did not complete within micronaut.servlet.websocket.connect-timeout (30 seconds by default), in which case the session is closed.

The connection itself is opened by the servlet container’s Jakarta client - the same ServerContainer the server side upgrades through is also a client container on Jetty, Tomcat and Undertow - so the Session a handler receives is the container’s own, and the handlers are invoked through the compiled metadata: the container never scans or instantiates the class. A @ClientEndpoint class compiled without the processor is handed to the container as it is, which then does both reflectively. @PathParam is rejected on a client endpoint, which has no URI template to bind it from, and Micronaut’s WebSocketClient does not open a @ClientEndpoint; a client for it is a @ClientWebSocket.

Filters and security run on the handshake

The upgrade request reaches the Micronaut servlet like any other request, so the Micronaut filter chain, including micronaut-security, runs against it before the connection is handed to the container. A filter that produces its own response cancels the upgrade and that response is written as an ordinary HTTP response, so @Secured on a @ServerWebSocket class rejects the handshake with a 401 rather than opening a socket.

Servlet filters registered with @ServletFilterBean still run before the Micronaut servlet, and therefore before Micronaut filters, exactly as they do for HTTP requests.

Origin checking

When the endpoint declares @CrossOrigin, or when micronaut.server.cors.enabled is set, the handshake is refused unless the Origin header is same-origin or matches an allowed origin. A @CrossOrigin on the endpoint states the policy for that endpoint and is used on its own; otherwise the global CORS configurations apply. This check happens during the handshake rather than on the response, because a browser applies its usual CORS response check to ordinary requests but not to a WebSocket handshake, so nothing else would stop a hostile page from opening an authenticated socket.

With no @CrossOrigin and CORS disabled every origin is accepted, matching the Netty server.

@PreMatching filters do not run on the handshake. The upgrade is matched to its route before the filter chain runs, so only filters resolved after route matching apply. The Netty server behaves the same way.

Configuration

🔗
Table 1. Configuration Properties for ServletWebSocketConfiguration
Property Type Description Default value

micronaut.servlet.websocket.enabled

boolean

Whether WebSocket support is enabled. Defaults to {@code true}.

micronaut.servlet.websocket.max-text-message-size

int

The maximum size in bytes of an inbound text message. Default value (65536).

micronaut.servlet.websocket.max-binary-message-size

int

The maximum size in bytes of an inbound binary message. Default value (65536).

micronaut.servlet.websocket.idle-timeout

java.time.Duration

How long a session may stay idle before it is closed, or {@code null} to inherit {@code micronaut.server.idle-timeout}

micronaut.servlet.websocket.async-send-timeout

java.time.Duration

How long an asynchronous send may take before it fails, or {@code null} to use the container default

micronaut.servlet.websocket.connect-timeout

java.time.Duration

How long {@code WebSocketContainer#connectToServer} waits for a client endpoint’s {@code @OnOpen} to complete before failing the connection. Default value 30 seconds.

micronaut.servlet.websocket.max-pending-sends

int

The number of queued sends beyond which a session reports that it is not writable. Default value (64).

Message sizes and the idle timeout are always applied explicitly, because the three containers ship different defaults. The values above match those of the Netty server. When micronaut.servlet.websocket.idle-timeout is not set, micronaut.server.idle-timeout is used, and an idle session is closed with close code 1001.

An explicit maxPayloadLength on @OnMessage takes precedence over the configured message sizes for that endpoint.

Differences from the Netty server

  • maxPayloadLength bounds a whole message here, whereas on Netty it bounds a single frame. Handlers always receive whole messages on both servers, and a message over the limit closes the session with close code 1009.

  • WebSocketSession#getId() is the identifier the container assigns, not the Sec-WebSocket-Key header.

  • Outbound writes are queued per session with one write outstanding on the container at a time, because Jakarta containers reject a second asynchronous send while one is in flight. WebSocketSession#isWritable() reports false once more than micronaut.servlet.websocket.max-pending-sends writes are queued.

  • The HTTP request that produced the handshake is copied before the protocol switch, since a servlet request is only valid for the duration of its dispatch. Handler arguments bound from the request, such as @Header and @QueryValue, therefore see the values as they were at handshake time. The authenticated principal is carried over with it, including one supplied by container-managed authentication in a WAR.

  • The handshake enforces the configured CORS origins, which the Netty server does not.

  • Handlers for one session are dispatched as each message arrives rather than being serialised, so with a multi-threaded @ExecuteOn executor two messages on the same session can run concurrently and complete out of order, and a message handler can overlap @OnOpen. This matches the Netty server. An endpoint that needs ordering should either keep its handlers non-blocking, so they run on the container thread that delivered the message, or synchronize on the session itself.

Unsupported runtimes

WebSocket is not available on the built-in Java HTTP server runtime or on POJA. com.sun.net.httpserver.HttpServer has no protocol upgrade API and no ServletContext, and a POJA application answers one request at a time over a single stream, so neither can hold a long lived full duplex connection.

WAR deployment is supported on any container implementing Jakarta WebSocket 2.1. On an older container the upgrade is answered with a 501, and a warning is logged the first time an upgrade is attempted.

6 WAR Deployment

To deploy as a WAR file you need to make some adjustments to your dependencies.

First make the server you are using a developmentOnly dependency (or provided in Maven):

developmentOnly("io.micronaut.servlet:micronaut-http-server-jetty")
<dependency>
    <groupId>io.micronaut.servlet</groupId>
    <artifactId>micronaut-http-server-jetty</artifactId>
    <scope>provided</scope>
</dependency>

Then make sure you include micronaut-servlet-engine dependency in your build configuration:

implementation("io.micronaut.servlet:micronaut-servlet-engine")
<dependency>
    <groupId>io.micronaut.servlet</groupId>
    <artifactId>micronaut-servlet-engine</artifactId>
</dependency>

Then alter your build configuration to build a WAR file. In Gradle this can be done by applying the WAR plugin:

Applying the Gradle WAR plugin
plugins {
    id "war"
    id "application"
}

You can then build the WAR file and deploy it to the Servlet container as per the instructions provided by the container.

Micronaut will load using MicronautServletInitializer which registers the DefaultMicronautServlet instance.

6.1 Container version considerations

Micronaut 4.0.0 switched to using the new jakarta.servlet package for Servlet API classes. This means that Micronaut 4.0.0+ WAR files will not run on Servlet containers that do not support the jakarta.servlet package.

For example, Tomcat 10 switched to the jakarta.servlet package, so Micronaut 4.0.0 is required to run as a WAR on Tomcat 10. And Micronaut 4.0.0 WAR files cannot be run on Tomcat 9 or earlier.

If you have a Micronaut 3 based WAR file that you wish to deploy to Tomcat 10, you need to deploy it to the $CATALINA_BASE/webapps-javaee directory instead of the usual $CATALINA_BASE/webapps, and Tomcat will perform a conversion to jakarta.servlet.

6.2 Payara deployment

Payara scans application classes and libraries for Jakarta lifecycle annotations during deployment. Micronaut applications packaged as WAR files include internal lifecycle beans from Micronaut Context, for example io.micronaut.scheduling.io.watch.DefaultWatchThread, and Payara can reject those classes before Micronaut starts.

For Micronaut 4 and later, only Payara 6 or later is supported because Micronaut uses the jakarta.servlet API.

When deploying to Payara, add an application web.xml with metadata-complete="true" so the container does not try to discover lifecycle annotations from Micronaut libraries:

src/main/webapp/WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
         version="6.0"
         metadata-complete="true">
</web-app>

This deployment descriptor complements the web-fragment.xml packaged in micronaut-servlet-engine and prevents Payara from treating Micronaut internal lifecycle methods as container lifecycle callbacks.

6.3 External Configuration

In a standalone Micronaut Framework application, external property sources for configuration can be configured via the Java system property micronaut.config.files or the environment variable MICRONAUT_CONFIG_FILES. When running as a WAR file, this can be problematic if you wish to run multiple Micronaut Framework WARs in the same container, as all applications will share the same location.

To allow an external location per application, it is necessary to write our own replacement for MicronautServletInitializer. The following example will look for configuration in the /tmp directory and on the classpath in the /some/path directory.

src/main/java/example/CustomMicronautServletInitializer.java
package example;

import io.micronaut.context.ApplicationContext;
import io.micronaut.context.ApplicationContextBuilder;
import io.micronaut.servlet.engine.initializer.MicronautServletInitializer;
import jakarta.servlet.ServletContext;

public class CustomInitializer extends MicronautServletInitializer {

    @Override
    protected ApplicationContextBuilder buildApplicationContext(ServletContext ctx) {
        return ApplicationContext
                .builder()
                .overrideConfigLocations(
                    "file:/tmp",
                    "classpath:/some/path"
                )
                .classLoader(ctx.getClassLoader())
                .singletons(ctx);
    }
}

We can then use Java’s Service Provider Interface to register this class as the initializer by pointing to it in the META-INF/services/jakarta.servlet.ServletContainerInitializer file.

src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer
example.CustomInitializer
Some servlet containers may limit the locations that are accessible from applications for security reasons.

6.4 WAR Context path

If you are deploying the WAR to the root context — for example by renaming the WAR to ROOT.war prior to deployment — then the context path may be overridden by configuring micronaut.server.context-path in your application configuration.

If you are deploying a WAR called myproject-1.0.war to Tomcat, Jetty, etc. the context path will be set to /myproject-1.0, and cannot be overridden via application configuration.

7 Jetty Server

To use Jetty as a server add the following dependency:

implementation("io.micronaut.servlet:micronaut-http-server-jetty")
<dependency>
    <groupId>io.micronaut.servlet</groupId>
    <artifactId>micronaut-http-server-jetty</artifactId>
</dependency>

Jetty is supported with GraalVM native image
If you plan to produce a WAR file then the dependency should be developmentOnly.

To customize the Jetty server you can use the following configuration properties:

🔗
Table 1. Configuration Properties for JettyConfiguration
Property Type Description Default value

micronaut.server.http-version

HttpVersion

micronaut.server.thread-selection

ThreadSelection

micronaut.server.redispatch-non-blocking-only

boolean

micronaut.server.default-charset

java.nio.charset.Charset

micronaut.server.port

int

micronaut.server.host

java.lang.String

micronaut.server.read-timeout

java.lang.Integer

micronaut.server.max-request-size

long

micronaut.server.max-request-buffer-size

long

micronaut.server.read-idle-timeout

java.time.Duration

micronaut.server.write-idle-timeout

java.time.Duration

micronaut.server.idle-timeout

java.time.Duration

micronaut.server.server-header

java.lang.String

micronaut.server.date-header

boolean

micronaut.server.log-handled-exceptions

boolean

micronaut.server.error-response-include-message

HttpServerConfiguration$ErrorResponseIncludeMessageMode

micronaut.server.client-address-header

java.lang.String

micronaut.server.context-path

java.lang.String

micronaut.server.dual-protocol

boolean

micronaut.server.http-to-https-redirect

boolean

micronaut.server.dispatch-options-requests

boolean

micronaut.server.validate-url

boolean

micronaut.server.escape-html-url

boolean

micronaut.server.not-found-on-missing-body

boolean

micronaut.server.semicolon-is-normal-char

boolean

micronaut.server.max-params

int

micronaut.server.jetty.enabled

boolean

Sets whether the Jetty runtime is enabled.

micronaut.server.jetty.native-static-resources

boolean

Serve static resources with Jetty’s own {@code ResourceHandler} instead of the Micronaut static resource resolver. Jetty’s handler supports range requests and conditional caching natively, but sits outside the Micronaut filter chain, so Micronaut filters (including CORS) do not see those requests and the {@code cache-control} setting of each static resource configuration applies instead of {@code micronaut.server.responses.file}. Default value (false).

micronaut.server.jetty.init-parameters

java.util.Map

Sets the servlet init parameters.

To disable the Jetty runtime when it is present on the classpath:

micronaut.server.jetty.enabled=false
micronaut.server.jetty.enabled: false
"micronaut.server.jetty.enabled" = false
micronaut.server.jetty.enabled = false
{
  "micronaut.server.jetty.enabled" = false
}
{
  "micronaut.server.jetty.enabled": false
}

This is useful when multiple servlet runtimes are available and you want Micronaut to ignore Jetty during startup.

Or you can register a BeanCreatedEventListener:

Jetty Server Customization
import io.micronaut.context.event.BeanCreatedEvent;
import io.micronaut.context.event.BeanCreatedEventListener;
import org.eclipse.jetty.server.Server;
import jakarta.inject.Singleton;

@Singleton
public class JettyServerCustomizer implements BeanCreatedEventListener<Server> {
    @Override
    public Server onCreated(BeanCreatedEvent<Server> event) {
        Server jettyServer = event.getBean();
        // perform customizations...
        return jettyServer;
    }
}

Static Resources

Static resources configured with micronaut.router.static-resources are served by the Micronaut static resource resolver, as on the other runtimes, so filters, CORS and the micronaut.server.responses.file settings apply to them. Because the resolver runs inside the Micronaut servlet, static resource paths must fall under micronaut.servlet.mapping (the default / covers everything); a narrowed mapping such as /api/ leaves paths outside it unreachable, as on Tomcat and Undertow. To have Jetty’s own ResourceHandler serve them instead, which supports range requests natively, works outside the servlet mapping, but bypasses the Micronaut filter chain, set:

micronaut.server.jetty.native-static-resources=true
micronaut.server.jetty.native-static-resources: true
"micronaut.server.jetty.native-static-resources" = true
micronaut.server.jetty.nativeStaticResources = true
{
  "micronaut.server.jetty.native-static-resources" = true
}
{
  "micronaut.server.jetty.native-static-resources": true
}

Access Log Configuration

To configure the Jetty Access Log:

Jetty Access Log Configuration
micronaut.server.jetty.access-log.enabled=true
micronaut.server.jetty.access-log.filename=/tmp/access.log
micronaut.server.jetty.access-log.retain-days=10
micronaut.server.jetty.access-log.pattern=%{client}a - %u %t "%r" %s %O
micronaut.server.jetty.access-log.enabled: true
micronaut.server.jetty.access-log.filename: /tmp/access.log
micronaut.server.jetty.access-log.retain-days: 10
micronaut.server.jetty.access-log.pattern: >
    %{client}a - %u %t "%r" %s %O
"micronaut.server.jetty.access-log.enabled" = true
"micronaut.server.jetty.access-log.filename" = "/tmp/access.log"
"micronaut.server.jetty.access-log.retain-days" = 10
"micronaut.server.jetty.access-log.pattern" = "%{client}a - %u %t "%r" %s %O"
micronaut.server.jetty.accessLog.enabled = true
micronaut.server.jetty.accessLog.filename = "/tmp/access.log"
micronaut.server.jetty.accessLog.retainDays = 10
micronaut.server.jetty.accessLog.pattern = "%{client}a - %u %t \"%r\" %s %O"
{
  "micronaut.server.jetty.access-log.enabled" = true
  "micronaut.server.jetty.access-log.filename" = "/tmp/access.log"
  "micronaut.server.jetty.access-log.retain-days" = 10
  "micronaut.server.jetty.access-log.pattern" = "%{client}a - %u %t \"%r\" %s %O"
}
{
  "micronaut.server.jetty.access-log.enabled": true,
  "micronaut.server.jetty.access-log.filename": "/tmp/access.log",
  "micronaut.server.jetty.access-log.retain-days": 10,
  "micronaut.server.jetty.access-log.pattern": "%{client}a - %u %t \"%r\" %s %O"
}

Jetty support for Logback access

If you want to use Logback access library with jetty you have to provide logback dependency on classpath:

implementation("ch.qos.logback.access:logback-access-common")
<dependency>
    <groupId>ch.qos.logback.access</groupId>
    <artifactId>logback-access-common</artifactId>
</dependency>

By default, it will check for logback-access.xml in resources folder.

8 Tomcat Server

To use Tomcat as a server add the following dependency:

implementation("io.micronaut.servlet:micronaut-http-server-tomcat")
<dependency>
    <groupId>io.micronaut.servlet</groupId>
    <artifactId>micronaut-http-server-tomcat</artifactId>
</dependency>

Tomcat is supported with GraalVM native image
If you plan to produce a WAR file then the dependency should be developmentOnly.

To customize the Tomcat server you can use the following configuration properties:

🔗
Table 1. Configuration Properties for TomcatConfiguration
Property Type Description Default value

micronaut.server.http-version

HttpVersion

micronaut.server.thread-selection

ThreadSelection

micronaut.server.redispatch-non-blocking-only

boolean

micronaut.server.default-charset

java.nio.charset.Charset

micronaut.server.port

int

micronaut.server.host

java.lang.String

micronaut.server.read-timeout

java.lang.Integer

micronaut.server.max-request-size

long

micronaut.server.max-request-buffer-size

long

micronaut.server.read-idle-timeout

java.time.Duration

micronaut.server.write-idle-timeout

java.time.Duration

micronaut.server.idle-timeout

java.time.Duration

micronaut.server.server-header

java.lang.String

micronaut.server.date-header

boolean

micronaut.server.log-handled-exceptions

boolean

micronaut.server.error-response-include-message

HttpServerConfiguration$ErrorResponseIncludeMessageMode

micronaut.server.client-address-header

java.lang.String

micronaut.server.context-path

java.lang.String

micronaut.server.dual-protocol

boolean

micronaut.server.http-to-https-redirect

boolean

micronaut.server.dispatch-options-requests

boolean

micronaut.server.validate-url

boolean

micronaut.server.escape-html-url

boolean

micronaut.server.not-found-on-missing-body

boolean

micronaut.server.semicolon-is-normal-char

boolean

micronaut.server.max-params

int

micronaut.server.tomcat.protocol

java.lang.String

The protocol to use. Defaults to org.apache.coyote.http11.Http11NioProtocol.

micronaut.server.tomcat.enabled

boolean

Sets whether the Tomcat runtime is enabled.

To disable the Tomcat runtime when it is present on the classpath:

micronaut.server.tomcat.enabled=false
micronaut.server.tomcat.enabled: false
"micronaut.server.tomcat.enabled" = false
micronaut.server.tomcat.enabled = false
{
  "micronaut.server.tomcat.enabled" = false
}
{
  "micronaut.server.tomcat.enabled": false
}

This is useful when multiple servlet runtimes are available and you want Micronaut to ignore Tomcat during startup.

Or you can register a BeanCreatedEventListener:

Tomcat Server Customization
import io.micronaut.context.event.BeanCreatedEvent;
import io.micronaut.context.event.BeanCreatedEventListener;
import org.apache.catalina.startup.Tomcat;
import jakarta.inject.Singleton;

@Singleton
public class TomcatServerCustomizer implements BeanCreatedEventListener<Tomcat> {
    @Override
    public Tomcat onCreated(BeanCreatedEvent<Tomcat> event) {
        Tomcat tomcat = event.getBean();
        // perform customizations...
        return tomcat;
    }
}

Access Log Configuration

To configure the Tomcat Access Log:

Tomcat Access Log Configuration
micronaut.server.tomcat.access-log.enabled=true,
micronaut.server.tomcat.access-log.pattern=combined
micronaut.server.tomcat.access-log.directory=/var/logs
micronaut.server.tomcat.access-log.enabled: true,
micronaut.server.tomcat.access-log.pattern: combined
micronaut.server.tomcat.access-log.directory: /var/logs
"micronaut.server.tomcat.access-log.enabled" = "true,"
"micronaut.server.tomcat.access-log.pattern" = "combined"
"micronaut.server.tomcat.access-log.directory" = "/var/logs"
micronaut.server.tomcat.accessLog.enabled = "true,"
micronaut.server.tomcat.accessLog.pattern = "combined"
micronaut.server.tomcat.accessLog.directory = "/var/logs"
{
  "micronaut.server.tomcat.access-log.enabled" = "true,"
  "micronaut.server.tomcat.access-log.pattern" = "combined"
  "micronaut.server.tomcat.access-log.directory" = "/var/logs"
}
{
  "micronaut.server.tomcat.access-log.enabled": "true,",
  "micronaut.server.tomcat.access-log.pattern": "combined",
  "micronaut.server.tomcat.access-log.directory": "/var/logs"
}

9 Undertow Server

To use Undertow as a server add the following dependency:

implementation("io.micronaut.servlet:micronaut-http-server-undertow")
<dependency>
    <groupId>io.micronaut.servlet</groupId>
    <artifactId>micronaut-http-server-undertow</artifactId>
</dependency>

Undertow is not supported with GraalVM native image. Use Jetty or Tomcat if native image support is required. See UNDERTOW-1408.
If you plan to produce a WAR file then the dependency should be developmentOnly.

To customize the Undertow server you can use the following configuration properties:

🔗
Table 1. Configuration Properties for UndertowConfiguration
Property Type Description Default value

micronaut.server.http-version

HttpVersion

micronaut.server.thread-selection

ThreadSelection

micronaut.server.redispatch-non-blocking-only

boolean

micronaut.server.default-charset

java.nio.charset.Charset

micronaut.server.port

int

micronaut.server.host

java.lang.String

micronaut.server.read-timeout

java.lang.Integer

micronaut.server.max-request-size

long

micronaut.server.max-request-buffer-size

long

micronaut.server.read-idle-timeout

java.time.Duration

micronaut.server.write-idle-timeout

java.time.Duration

micronaut.server.idle-timeout

java.time.Duration

micronaut.server.server-header

java.lang.String

micronaut.server.date-header

boolean

micronaut.server.log-handled-exceptions

boolean

micronaut.server.error-response-include-message

HttpServerConfiguration$ErrorResponseIncludeMessageMode

micronaut.server.client-address-header

java.lang.String

micronaut.server.context-path

java.lang.String

micronaut.server.dual-protocol

boolean

micronaut.server.http-to-https-redirect

boolean

micronaut.server.dispatch-options-requests

boolean

micronaut.server.validate-url

boolean

micronaut.server.escape-html-url

boolean

micronaut.server.not-found-on-missing-body

boolean

micronaut.server.semicolon-is-normal-char

boolean

micronaut.server.max-params

int

micronaut.server.undertow.enabled

boolean

Sets whether the Undertow runtime is enabled.

micronaut.server.undertow.worker-options

java.util.Map

Sets the worker options.

micronaut.server.undertow.socket-options

java.util.Map

Sets the socket options.

micronaut.server.undertow.server-options

java.util.Map

The server options.

To disable the Undertow runtime when it is present on the classpath:

micronaut.server.undertow.enabled=false
micronaut.server.undertow.enabled: false
"micronaut.server.undertow.enabled" = false
micronaut.server.undertow.enabled = false
{
  "micronaut.server.undertow.enabled" = false
}
{
  "micronaut.server.undertow.enabled": false
}

This is useful when multiple servlet runtimes are available and you want Micronaut to ignore Undertow during startup.

Or you can register a BeanCreatedEventListener:

Undertow Server Customization
import io.micronaut.context.event.BeanCreatedEvent;
import io.micronaut.context.event.BeanCreatedEventListener;
import io.undertow.Undertow;
import jakarta.inject.Singleton;

@Singleton
public class UndertowServerCustomizer implements BeanCreatedEventListener<Undertow.Builder> {
    @Override
    public Undertow.Builder onCreated(BeanCreatedEvent<Undertow.Builder> event) {
        Undertow.Builder undertowBuilder = event.getBean();
        // perform customizations...
        return undertowBuilder;
    }
}

Access Log Configuration

To configure the Undertow Access Log:

Undertow Access Log Configuration
micronaut.server.undertow.access-log.enabled=true,
micronaut.server.undertow.access-log.pattern=combined
micronaut.server.undertow.access-log.output-directory=/var/logs
micronaut.server.undertow.access-log.enabled: true,
micronaut.server.undertow.access-log.pattern: combined
micronaut.server.undertow.access-log.output-directory: /var/logs
"micronaut.server.undertow.access-log.enabled" = "true,"
"micronaut.server.undertow.access-log.pattern" = "combined"
"micronaut.server.undertow.access-log.output-directory" = "/var/logs"
micronaut.server.undertow.accessLog.enabled = "true,"
micronaut.server.undertow.accessLog.pattern = "combined"
micronaut.server.undertow.accessLog.outputDirectory = "/var/logs"
{
  "micronaut.server.undertow.access-log.enabled" = "true,"
  "micronaut.server.undertow.access-log.pattern" = "combined"
  "micronaut.server.undertow.access-log.output-directory" = "/var/logs"
}
{
  "micronaut.server.undertow.access-log.enabled": "true,",
  "micronaut.server.undertow.access-log.pattern": "combined",
  "micronaut.server.undertow.access-log.output-directory": "/var/logs"
}

10 Built-In Java HTTP Server Runtime

To use a server runtime based on the Java built-in Http Server, add the following dependency:

implementation("io.micronaut.servlet:micronaut-http-server-jdk")
<dependency>
    <groupId>io.micronaut.servlet</groupId>
    <artifactId>micronaut-http-server-jdk</artifactId>
</dependency>

To customize the built-in JDK HTTP server runtime you can use the following configuration properties:

🔗
Table 1. Configuration Properties for JdkHttpServerConfiguration
Property Type Description Default value

micronaut.server.http-version

HttpVersion

micronaut.server.thread-selection

ThreadSelection

micronaut.server.redispatch-non-blocking-only

boolean

micronaut.server.default-charset

java.nio.charset.Charset

micronaut.server.port

int

micronaut.server.host

java.lang.String

micronaut.server.read-timeout

java.lang.Integer

micronaut.server.max-request-size

long

micronaut.server.max-request-buffer-size

long

micronaut.server.read-idle-timeout

java.time.Duration

micronaut.server.write-idle-timeout

java.time.Duration

micronaut.server.idle-timeout

java.time.Duration

micronaut.server.server-header

java.lang.String

micronaut.server.date-header

boolean

micronaut.server.log-handled-exceptions

boolean

micronaut.server.error-response-include-message

HttpServerConfiguration$ErrorResponseIncludeMessageMode

micronaut.server.client-address-header

java.lang.String

micronaut.server.context-path

java.lang.String

micronaut.server.dual-protocol

boolean

micronaut.server.http-to-https-redirect

boolean

micronaut.server.dispatch-options-requests

boolean

micronaut.server.validate-url

boolean

micronaut.server.escape-html-url

boolean

micronaut.server.not-found-on-missing-body

boolean

micronaut.server.semicolon-is-normal-char

boolean

micronaut.server.max-params

int

micronaut.server.jdk.enabled

boolean

Sets whether the JDK HTTP server runtime is enabled.

To disable the built-in JDK HTTP server runtime when it is present on the classpath:

micronaut.server.jdk.enabled=false
micronaut.server.jdk.enabled: false
"micronaut.server.jdk.enabled" = false
micronaut.server.jdk.enabled = false
{
  "micronaut.server.jdk.enabled" = false
}
{
  "micronaut.server.jdk.enabled": false
}

HTTPS

The built-in server supports TLS through the same SSL configuration as the other runtimes. With micronaut.ssl.enabled set to true the server listens for HTTPS on micronaut.server.ssl.port (default 8443, or a random port in the test environment) instead of the HTTP port. The key store, trust store, protocols, ciphers and client authentication are read from micronaut.server.ssl. Unlike the servlet containers the built-in server has a single listener, so it does not also serve plain HTTP when TLS is enabled.

Access Log

The built-in server can write an access log in the common log format, one line per request, to an SLF4J logger:

micronaut.server.jdk.access-logger.enabled=true
micronaut.server.jdk.access-logger.logger-name=HTTP_ACCESS_LOGGER
micronaut.server.jdk.access-logger.exclusions[0]=/health.*
micronaut:
  server:
    jdk:
      access-logger:
        enabled: true
        logger-name: HTTP_ACCESS_LOGGER
        exclusions:
          - /health.*
micronaut = {server = {jdk = {access-logger = {enabled = true, logger-name = "HTTP_ACCESS_LOGGER", exclusions = ["/health.*"]}}}}
micronaut {
  server {
    jdk {
      accessLogger {
        enabled = true
        loggerName = "HTTP_ACCESS_LOGGER"
        exclusions = ["/health.*"]
      }
    }
  }
}
{
  micronaut {
    server {
      jdk {
        access-logger {
          enabled = true
          logger-name = "HTTP_ACCESS_LOGGER"
          exclusions = ["/health.*"]
        }
      }
    }
  }
}
{
  "micronaut": {
    "server": {
      "jdk": {
        "access-logger": {
          "enabled": true,
          "logger-name": "HTTP_ACCESS_LOGGER",
          "exclusions": ["/health.*"]
        }
      }
    }
  }
}

logger-name defaults to HTTP_ACCESS_LOGGER, as on the Netty server, and exclusions are regular expressions matched against the request path. Beans of type com.sun.net.httpserver.Filter are applied to every context of the server in bean order, which is how the access log is installed and can be used for other per-exchange concerns.

11 HTTP POJA Application

HTTP POJA allows creating Micronaut applications that consume and respond to HTTP requests with streams. By default, the application will read requests from standard input stream and write responses to standard output. The requests can only be answered in a serial manner. Blocking work also stays on the POJA request thread by default, so using @ExecuteOn(TaskExecutors.BLOCKING) does not create additional Micronaut executor threads for POJA applications. If you need different behavior, configure the blocking executor explicitly with the standard micronaut.executors.blocking.* properties. Currently HTTP POJA is based on Apache HTTP Core library.

This feature allows creating simple applications that launch and respond on demand with minimal overhead. The module is suitable for usage with systemd on Linux or launchd on MacOS. Examples are given below.

To use the HTTP POJA feature add the following dependencies:

implementation("io.micronaut.servlet:micronaut-http-poja-apache")
<dependency>
    <groupId>io.micronaut.servlet</groupId>
    <artifactId>micronaut-http-poja-apache</artifactId>
</dependency>

testImplementation("io.micronaut.servlet:micronaut-http-poja-test")
<dependency>
    <groupId>io.micronaut.servlet</groupId>
    <artifactId>micronaut-http-poja-test</artifactId>
    <scope>test</scope>
</dependency>

To customize the HTTP POJA you can use the following configuration properties:

🔗
Table 1. Configuration Properties for ApacheServletConfiguration
Property Type Description Default value

poja.apache.input-buffer-size

int

The size of the buffer that is used to read and parse the HTTP request (in bytes). Default value is 8192 (8Kb).

8192

poja.apache.output-buffer-size

int

The size of the buffer that is used to write the HTTP response (in bytes). Default value is 8192 (8Kb).

8192

poja.apache.use-inherited-channel

boolean

When true, the inherited channel will be used by if present. Otherwise, STDIN and STDOUT will be used.

true

🔗
Table 2. Configuration Properties for ApacheRuntimeConfiguration
Property Type Description Default value

poja.apache.enabled

boolean

Sets whether the Apache POJA runtime is enabled.

To disable the Apache HTTP POJA runtime when it is present on the classpath:

poja.apache.enabled=false
poja.apache.enabled: false
"poja.apache.enabled" = false
poja.apache.enabled = false
{
  "poja.apache.enabled" = false
}
{
  "poja.apache.enabled": false
}

This is useful when multiple Micronaut runtimes are available and you want Micronaut to ignore the POJA runtime during startup.

Use HTTP POJA with launchd on MacOS

If you have built a HTTP POJA application as a native image executable, create the following plist file and replace [executable] with your executable path.

If you are unfamiliar with building native image executables refer to Micronaut Creating First Graal App guide.
If you do not wish to use native image prepend java and -jar program arguments and use the jar instead.
~/Library/LaunchAgents/com.example.poja.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>Label</key>
    <string>com.example.poja</string>

    <key>Enabled</key>
    <false/>

    <key>ProgramArguments</key>
    <array>
        <string>[executable]</string>
        <string>-Dpoja.apache.useInheritedChannel=false</string>
    </array>

    <key>Sockets</key>
    <dict>
      <key>Listeners</key>
      <dict>
        <key>SockServiceName</key>
        <string>8080</string>
        <key>SockType</key>
        <string>stream</string>
        <key>SockProtocol</key>
        <string>TCP</string>
      </dict>
    </dict>

    <key>StandardErrorPath</key>
    <string>/tmp/com.example.poja.log</string>

    <key>inetdCompatibility</key>
    <dict>
      <key>Wait</key>
      <false/>
    </dict>

    <key>KeepAlive</key>
    <false/>
  </dict>
</plist>

Load the plist file with launchd:

launchctl load ~/Library/LaunchAgents/com.example.poja.plist

Then the configured application will respond on port 8080:

curl localhost:8080

Use HTTP POJA with systemd on Linux

If you have built a HTTP POJA application as a native image executable, create the following files and replace [executable] with your executable path.

If you are unfamiliar with building native image executables refer to Micronaut Creating First Graal App guide.
If you do not wish to use native image prepend java and -jar program arguments and use the jar instead.
/etc/systemd/system/examplepoja.socket
[Unit]
Description=Socket to launch poja example on incoming connection

[Socket]
ListenStream=127.0.0.1:8080
Accept=yes

[Install]
WantedBy=sockets.target
/etc/systemd/system/examplepoja@.service
[Unit]
Description=Example Poja Service
Requires=examplepoja.socket

[Service]
Type=simple
ExecStart=[executable] -Dpoja.apache.useInheritedChannel=false
ExecStop=/bin/kill $MAINPID
KillMode=process
StandardInput=socket
StandardOutput=socket
StandardError=journal

[Install]
WantedBy=multi-user.target

Change selinux policy to allow systemd to use executable in the desired location with:

chcon -R -t bin_t [executable parent directory]

Enable and start listening on the socket with systemctl:

sudo systemctl enable examplepoja.socket
sudo systemctl start examplepoja.socket

Then the configured application will respond on port 8080:

curl localhost:8080

Use HTTP POJA with systemd-socket-activate on Linux

To test your application with systemd-socket-activate run:

systemd-socket-activate --inetd -a -l /tmp/http-poja.sock [executable]

In a separate terminal send a request to the socket:

curl --unix-socket /tmp/http-poja.sock http://localhost/

12 Parity with the Netty Server

The servlet runtimes implement the same Micronaut HTTP server contract as the default Netty server and are checked against the same HTTP server TCK. The table below records where each runtime stands for the capabilities that have historically differed. "Yes" means supported and covered by a test in this repository or in the TCK.

Capability Jetty Tomcat Undertow JDK HttpServer POJA Notes

Request bodies streamed as they arrive

Yes

Yes

Yes

Blocking

Blocking

Containers with asynchronous support read a large or chunked body through a ReadListener; a body with a declared length up to micronaut.server.max-request-buffer-size is read on the container thread before the route runs. Either way micronaut.server.max-request-size is the limit past which a body is refused. The JDK server and POJA read from a blocking stream.

Request without Content-Length or Transfer-Encoding has no body

Yes

Yes

Yes

Yes

No

RFC 9112 framing. POJA still decodes a body that was never sent.

Multipart: CompletedFileUpload, @Part, MultipartBody

Yes

Yes

Yes

No

No

Parsed by the container (getParts()), enabled by default since 6.2. An unannotated StreamingFileUpload argument is not supported. The JDK server and POJA do not parse multipart.

micronaut.server.max-request-size → 413

Yes

Yes

Yes

Yes

No

Applies to text, JSON, form and multipart bodies; a declared length over the limit is refused without reading the body.

Content-Length on complete responses

Yes

Yes

Yes

Yes

Yes

Bodies that are complete when the route returns announce their length; streamed bodies are chunked.

Body-less responses: HEAD, 204, 304

Yes

Yes

Yes

Yes

Yes

File responses: SystemFile, StreamedFile, conditional 304

Yes

Yes

Yes

Yes

Yes

Cache-Control and Last-Modified follow micronaut.server.responses.file. Byte ranges (206) are served by the containers, not by the JDK server or POJA.

Server-sent events

Yes

Yes

Yes

Yes

Yes

Events are flushed as they are produced.

Response compression

Yes

Yes

Yes

No

No

micronaut.servlet.compression.*, on by default, wired to each container’s own encoder. Compressed responses carry Vary: Accept-Encoding; Jetty sends it on every compressible response.

HTTP/2

Yes

Yes

Yes

No

No

micronaut.server.http-version: 2.0. Jetty and Tomcat support h2c; Undertow enables its native HTTP/2 over TLS.

TLS

Yes

Yes

Yes

Yes

No

Shared micronaut.server.ssl configuration. The containers listen on both ports; the JDK server listens for HTTPS only when TLS is enabled.

Virtual threads (micronaut.servlet.enable-virtual-threads)

Yes

Yes

Yes

Yes

No

Tomcat keeps its virtual-thread executor when max-threads is set, and logs that the limit does not apply; Undertow supplies one to the servlet deployment; the JDK server uses one for its handlers. Virtual threads are not pooled, so max-threads bounds concurrency only when they are off.

Graceful shutdown (micronaut.lifecycle.graceful-shutdown.enabled)

Yes

Yes

Yes

Yes

No

The server stops accepting (Jetty connector shutdown, Tomcat connector pause, Undertow 503) and waits for in-flight requests within the grace period. The JDK server only waits.

Access log

Yes

Yes

Yes

Yes

No

Each container’s own log; the JDK server under micronaut.server.jdk.access-logger.

ProxyHttpClient inside a server filter

Yes

Yes

Yes

Yes

No

The proxied response body is forwarded. POJA has no server to proxy to.

Cookie attributes (SameSite, Max-Age, several Set-Cookie)

Yes

Yes

Yes

Yes

Yes

Static resources: mappings, index.html, context path

Yes

Yes

Yes

Yes

Yes

One resolver on every runtime, inside the filter chain. Jetty’s own handler is available with micronaut.server.jetty.native-static-resources.

WebSocket (@ServerWebSocket)

Yes

Yes

Yes

No

No

Bridged onto each container’s Jakarta WebSocket implementation.

Runtime positioning

Jetty, Tomcat and Undertow are full runtimes for production use. The built-in JDK HttpServer runtime is intended for development and small tools: it has no HTTP/2, no compression and no multipart parsing, and it keeps at most 200 idle connections. POJA (plain old Java application) runs one request at a time over standard input and output and receives correctness fixes only.

13 Known Issues

There are some known issues with Servlet integration to the Micronaut Framework. See also Parity with the Netty server for what each runtime supports.

Payara deployment annotation scanning

Payara may scan Micronaut libraries in a WAR for Jakarta lifecycle annotations before Micronaut starts. When that happens, deployment can fail on internal Micronaut classes such as DefaultWatchThread.

Add an application src/main/webapp/WEB-INF/web.xml with metadata-complete="true" as described in the Payara deployment section.

Static resources outside the servlet mapping

Static resources are served by the Micronaut servlet, so their paths must fall under micronaut.servlet.mapping. A narrowed mapping such as /api/* leaves paths outside it unreachable on every container; on Jetty, micronaut.server.jetty.native-static-resources serves them with the container’s own handler instead.

Unannotated StreamingFileUpload arguments

A StreamingFileUpload controller argument without @Part is not bound on the servlet runtimes, because the container has already parsed the multipart body. Annotate the argument with @Part, or use CompletedFileUpload.

14 FAQ

Where can I find the source code?

You can find the source code of this project in this repository:

How do I configure Multipart handling?

Multipart handling is enabled by default, as it is on the Netty server. Set micronaut.server.multipart.enabled to false to turn it off, and see the configuration properties for the limits that apply to uploads.

How do I configure Static Resource handling for the embedded server?

Static resources are not enabled by default. See Serving Static Resources for how to configure paths to static resources.

How do I enable HTTPS for the embedded server?

How do I shut the server down gracefully?

Set micronaut.lifecycle.graceful-shutdown.enabled to true, as for the Netty server. When the application stops, the embedded server first stops taking new requests and then waits, for up to the configured grace period, for the requests already in progress to complete before the container is stopped. How new requests are refused depends on the container: Jetty stops accepting connections, Tomcat pauses its connectors, and Undertow answers new requests with 503 Service Unavailable. The built-in Java HTTP server only waits for in-flight requests.

15 Breaking Changes

This section documents breaking changes between Micronaut Servlet versions:

Micronaut Servlet 6.2.0

The request size limit applies to every body

micronaut.server.max-request-size (10MB by default) was only passed to the container’s multipart configuration, so a plain JSON, text or binary body of any size was accepted. The servlet runtimes now enforce it as the Netty server does: a body that declares a larger Content-Length is refused with 413 Request Entity Too Large without being read, and a body without a declared length is cut off with the same status once it grows past the limit. That includes a URL-encoded form without a declared length, whose fields are decoded from bytes that passed through the limit rather than parsed by the container unbounded. Raise micronaut.server.max-request-size for applications that receive larger bodies.

Jetty serves static resources through the Micronaut resolver

Jetty served micronaut.router.static-resources with its own ResourceHandler, outside the Micronaut servlet, while Tomcat, Undertow, the JDK server and the Netty server all resolve them with Micronaut’s StaticResourceResolver inside the request pipeline. Jetty now uses the shared path too, so static resources behave the same on every runtime:

  • Micronaut filters see static resource requests. In particular the CORS filter applies, so a request from an origin that is not allowed is refused with 403 Forbidden, where Jetty’s handler answered 200 without CORS headers.

  • The Cache-Control header comes from micronaut.server.responses.file (private, max-age=60 by default), and conditional requests with If-Modified-Since receive 304 Not Modified.

  • The Jetty-only micronaut.router.static-resources.*.cache-control setting is not applied.

Set micronaut.server.jetty.native-static-resources to true to restore Jetty’s ResourceHandler, including the cache-control setting and its own CORS handling.

Responses are compressed by default

Servlet containers did not compress responses at all, while the Netty server compresses anything above 1KB that the client accepts. micronaut.servlet.compression.enabled now defaults to true, and is wired to each container’s own encoder rather than reimplemented as a filter, so the cases that make compression awkward, HEAD, ranges, already encoded bodies and the Vary header, are settled by code that already handles them.

Two things change for Jetty, Tomcat and Undertow applications:

  • A response over micronaut.servlet.compression.threshold (1KB) whose content type is in micronaut.servlet.compression.content-types is sent gzipped when the client sends Accept-Encoding: gzip.

  • Those responses carry Vary: Accept-Encoding, which is required so a cache does not serve a compressed body to a client that cannot read it.

Set micronaut.servlet.compression.enabled to false to restore the previous behaviour. The JDK server runtime does not support compression.

Virtual threads take effect on Undertow, and set thread limits aside

micronaut.servlet.enable-virtual-threads has defaulted to true since Micronaut Servlet 4, but it only took effect on Jetty and Tomcat. Undertow ignored it and ran every servlet invocation on its XNIO worker pool, eight threads per core by default, so a blocking controller was capped at that pool’s size. On JDK 21 and later Undertow now runs the servlet deployment on a virtual thread per request, as the other containers do.

A consequence on Tomcat and Undertow: virtual threads are not pooled, so micronaut.servlet.max-threads and micronaut.servlet.min-threads no longer bound request concurrency while they are enabled. Tomcat used to replace its virtual-thread executor with a platform pool of that size, silently turning virtual threads off; it now keeps the virtual threads and logs a warning that the limit does not apply. Undertow’s WORKER_TASK_MAX_THREADS still sizes the worker pool that accepts connections, but not the threads that run routes. An application that relied on max-threads to cap load on a downstream resource, such as a JDBC pool, should set micronaut.servlet.enable-virtual-threads to false to size a platform pool as before, or bound the resource itself.

Multipart handling is enabled by default

micronaut.server.multipart.enabled has no default of its own. Its isEnabled() reported false when the property was unset, while the Netty server and the shared request lifecycle read the raw value and treat unset as enabled. The servlet configuration followed isEnabled(), so no MultipartConfigElement was registered and the container never parsed multipart bodies: parts reached no controller argument and getParts() was unavailable.

Servlet containers now follow the same rule as the rest of the framework, so an unset value means enabled.

Two consequences for Jetty, Tomcat and Undertow applications:

  • Multipart requests are parsed where they previously were not, so @Part arguments, CompletedFileUpload and getParts() start working without configuration.

  • Micronaut’s multipart limits now apply to those uploads. micronaut.server.multipart.max-file-size defaults to 1MB and micronaut.server.max-request-size to 10MB, so an upload that previously passed unchecked may now be rejected. Raise the limits, or set micronaut.server.multipart.enabled to false to restore the previous behaviour.

Micronaut Servlet 5.0.0

Deprecations

  • The Singleton constructor io.micronaut.servlet.engine.DefaultServletHttpHandler(ApplicationContext) deprecated previously has been removed. `DefaultServletHttpHandler(ApplicationContext, ConversionService) is used instead.

  • The abstract class constructor io.micronaut.servlet.http.ServletHttpHandler(ApplicationContext) deprecated previously has been removed. ServletHttpHandler(ApplicationContext, ConversionService) is used instead.

Micronaut Servlet 3.3.4

Binding network interface

Previously, the default servlet engine will bind to all network interfaces. This is a security risk. Now, the default servlet engine will bind to localhost only. To restore the original functionality, you need to configure micronaut.server.host, or set the HOST environment variable.

16 Repository

You can find the source code of this project in this repository: