Mu Server 3.0.0 migration guide

Mu Server 3 upgrades to Jakarta REST 3.1 and fixes a number of JAX-RS compatibility issues. These notes cover the changes from 2.4.3 to 3.0.0. If you are upgrading from an older 2.x version, see the changelog for the changes before 2.4.3.

Upgrading from 2.x

The Maven coordinates and Java package names are unchanged. To try the prerelease, use:

<dependency>
    <groupId>io.muserver</groupId>
    <artifactId>mu-server</artifactId>
    <version>0.0.3.12</version>
</dependency>

A typical application with unambiguous resource paths and one JSON provider may need no source changes beyond the dependency and Java upgrades. The default REST error response is now JSON, so check any clients that read error bodies. If you use custom providers, filters, interceptors, converters, generic resource interfaces, URI building, matrix parameters, async endpoints, or SSE, see the relevant changes below. Also check any code that uses the boolean returned by graceful shutdown.

QUERY and OpenAPI 3.2

Mu Server 3 supports the safe, idempotent HTTP QUERY method. Use Method.QUERY with a Mu handler, or annotate a Jakarta REST resource method with io.muserver.rest.QUERY:

@Path("/search")
public class SearchResource {
    @io.muserver.rest.QUERY
    @Consumes("text/plain")
    @Produces("text/plain")
    public String search(String query) {
        return "Result for " + query;
    }
}

A QUERY request needs a valid, concrete Content-Type, even when its body is empty. Mu Server uses @Consumes to select the resource method and advertises supported query body types in Accept-Query on automatic OPTIONS and unsupported-media-type responses.

Generated OpenAPI JSON now declares "openapi": "3.2.1". A resource method like the one above appears as a query operation with a request body and response. SSE responses can also describe the events clients receive. Add the repeatable io.muserver.rest.ApiSseEvent annotation to a Jakarta REST streaming method, specifying each event name, payload type, and payload media type:

@GET
@Path("/prices")
@Produces(MediaType.SERVER_SENT_EVENTS)
@io.muserver.rest.ApiSseEvent(name = "price", data = Integer.class,
    mediaType = "application/json", description = "Current price")
public void prices(@Context SseEventSink sink, @Context Sse sse) {
    sink.send(sse.newEventBuilder().name("price")
        .mediaType(MediaType.APPLICATION_JSON_TYPE)
        .data(Integer.class, 42).build())
        .whenComplete((ignored, error) -> sink.close());
}

The generated text/event-stream response uses OpenAPI 3.2's itemSchema to describe a parsed event with event, data, id, and retry fields. For the example above, event is fixed to price, and the string data field has contentMediaType: application/json and a contentSchema for Integer. Add more @ApiSseEvent annotations for other named events; they become alternatives in the itemSchema. The annotation documents the stream and does not change what the method sends.

HTTP/2 request validation

HTTP/2 requests are rejected if Host does not match :authority.

Jakarta REST support

Mu Server 3 follows the Jakarta REST specification more closely, including cases where 2.x behaved differently from other implementations. The changes are tested with Mu Server's regression tests and a server-side test harness based on the Jakarta REST 3.1 Technology Compatibility Kit (TCK). Mu Server is not certified, but passes all TCK tests for the features it declares support for.

As in 2.x, Mu Server implements a subset of Jakarta REST, with explicitly registered singleton resources and providers. It does not support EntityPart, the standard List<EntityPart> multipart providers, Feature/DynamicFeature service loading, per-request resource injection, or bean validation. See the Mu Server spec implementation page for the full list.

Behaviour changes

REST exception responses

Mu Server 3 registers an exception mapper for Throwable by default on Jakarta REST (JAX-RS) handlers. This does not change error handling for ordinary Mu handlers. Application-specific exception mappers still take precedence, and a WebApplicationException that already has an entity keeps its original response.

For example, if a resource throws new IllegalStateException("database path: /secret"), the response is still 500 Internal Server Error, but the body is now application/problem+json instead of Mu Server's standard HTML error page. It includes the detail An unexpected error occurred and a unique urn:uuid: instance ID, with a Cache-Control: no-store header. The exception and instance ID are logged; the exception message is not sent to the client.

This also applies to expected errors thrown by resource methods, such as NotFoundException and BadRequestException, when they have no explicit response entity. For example, new NotFoundException("No user with id 4") still returns 404 Not Found, but the HTML error body becomes application/problem+json, with No user with id 4 as the title. Check clients that parse these error bodies as well as those handling unexpected errors.

A malformed Accept header, such as Accept: text, still returns 400 Bad Request, also with application/problem+json instead of the standard error response.

To retain the 2.x behaviour of returning Mu Server's HTML error page for exceptions without an application mapper, remove the default mapper:

restHandler(resource)
    .removeExceptionMapper(Throwable.class);

Async resource errors

If a resource returns a CompletionStage that fails, Mu Server now passes the error to the exception mapper and completes the HTTP response, just as it would for a synchronous resource. In 2.x the request could hang until a timeout or disconnect, even though the future had already failed.

For example, returning CompletableFuture.failedFuture(new NotFoundException("Order not found")) now produces a 404 with the default exception mapper. CompletionException and ExecutionException wrappers are unwrapped so that the underlying exception selects the mapper. A WebApplicationException with its own response entity retains that response.

Cancelled futures also go through exception mapping and return 500 by default. You can register an exception mapper to change this response. When upgrading, check failed and cancelled futures as well as successful ones.

Graceful shutdown

Fixed the boolean returned by MuServer.stop(...) when stopping gracefully with an in-flight request. It now returns true if the request finishes within the timeout and the server shuts down cleanly, or false if requests do not finish in time. In 2.x these values were reversed. The result now matches the documented behaviour.

HTTP/2 completion listeners

Completion listeners now run after both the request and response have finished. In Mu Server 2, an HTTP/2 response could finish before the request body, causing the listener to observe completedSuccessfully() == false even when the request subsequently completed normally. In 3.x, a completed upload reports success; a reset or disconnect during the remaining upload reports failure. Check applications that use completion listeners for metrics or cleanup.

Resource matching, parameters, and locations

Several changes have been made to resource matching, parameter handling, and response locations:

Matrix parameters and generated OpenAPI

@MatrixParam now reads the last path segment matched at the current resource or locator, rather than the final segment of the entire request. For example, in /cars;colour=red/wheels;colour=black, a locator matching cars receives red and a method matching wheels receives black. Check applications that relied on the locator receiving the final segment's value.

Generated OpenAPI now includes matrix parameters at their corresponding path segments. Path templates and parameter names can therefore change; regenerate client code and review the resulting request URLs. URI building also treats segment("a/b;c") as one segment, escaping the slash and semicolon. replaceMatrixParam can add a previously absent parameter and removes it when given null or no values; replacePath(null) clears the path.

Spaces, plus signs, and percent encoding

HTML form encoding treats + as an encoded space, while URI path and matrix components treat a literal + as a plus sign. Mu Server 3 applies the rules for each component separately. Static resource handlers also preserve literal plus signs in filenames: /a+b.txt now looks for a+b.txt, rather than a b.txt. Query and form decoding still treat plus as a space.

UriBuilder now handles complete UTF-8 escape sequences, including encoded Unicode such as caf%C3%A9, without corrupting the characters. Percent-encoded values are decoded once, and encoded path and matrix delimiters remain part of their component. For example, %252F stays an encoded literal %2F, and a%2Fb stays one path segment.

Malformed UTF-8 encountered while decoding REST paths or matrix parameters now produces a 400 Bad Request response. Test clients that previously relied on replacement characters for invalid encodings.

Some examples of the encoding changes:

Example Mu Server 2 Mu Server 3
Write blue green and blue+green as application/x-www-form-urlencoded blue%20green and blue%2Bgreen blue+green and blue%2Bgreen
Read q=blue+green with @Encoded @FormParam("q") String q blue%20green blue+green
Read /cars/blue+green with decoded UriInfo path access cars/blue green cars/blue+green
Read the matrix parameter ;colour=blue+green%20car colour=blue green car colour=blue+green car

When building path or query components, blue green and blue+green are now consistently encoded as blue%20green and blue%2Bgreen respectively.

UriInfo.getAbsolutePath() now preserves the request authority and encoded slashes such as %2F. Previously it could use the configured base authority instead, or treat %2F as a path delimiter.

Providers, filters, and interceptors

Custom message body readers and writers now take precedence over built-in providers. For example, if you register a provider for Object, it is used for a String entity too. Previously the built-in String provider could be selected instead.

There are also fixes to provider selection:

Application readers and writers now use @Priority to break ties. Lower numbers take precedence; the default is Priorities.USER (5000). Readers are selected by media-type specificity, then priority. Writers are selected by Java type, then media-type specificity, then priority. Exception mappers registered through Application also use priority to break ties for the same exception type.

Request filters and reader/writer interceptors run in ascending priority order: 100 runs before 200. Response filters run in descending order. Previously interceptor order could depend on registration order, with a reader interceptor added second running first.

Other filter and interceptor fixes:

Request filters can now replace the entity stream with a GZIPInputStream and remove the old Content-Encoding and Content-Length headers without causing failed or truncated gzip decoding. The resource receives the complete decompressed body, and the removed headers stay absent.

Replacement response streams are now closed, so a response filter using GZIPOutputStream produces a complete gzip body.

If your filter uses GZIPOutputStream, set the HTTP status and headers before creating the stream. Also remove any Content-Length calculated for the uncompressed body, as compression changes its size. Creating the stream immediately writes the gzip header and can start sending the response to the client. Once the HTTP status and headers have been sent, they cannot be changed.

Writer interceptors

In Mu Server 3, the final WriterInterceptorContext.proceed() call invokes the message body writer before returning. In 2.x, the body was written after all writer interceptors had returned. Code after proceed() now runs after serialisation, and exceptions from the writer propagate through the interceptors before reaching exception mapping.

Set the entity, media type, annotations, and output stream before calling proceed(). Changing the entity after that call no longer changes the serialised body. Code that finishes compression, inspects captured bytes, or appends output can run after proceed(); Mu closes the final replacement stream after the interceptors return. Headers must still be set before output commits the response.

An interceptor that returns without calling proceed() now stops the chain: later interceptors and the body writer do not run. Add that call if your interceptor expects normal serialisation to continue. Mu also avoids calculating Content-Length from the original entity when a writer interceptor runs, since the interceptor can change the bytes sent. Applications that set this header themselves must supply the final transmitted length.

Responses, cookies, and variants

An empty text/plain request entity for boxed types such as Integer now produces 400 Bad Request, rather than passing null to the resource. Built-in text providers also support BigDecimal and BigInteger. These rules concern whole request entities, not absent query parameters.

New features

Providers and application context

Resource methods can now receive @Context Providers and @Context Application. Register context resolvers with RestHandlerBuilder.addContextResolver(ContextType.class, resolver), or supply them through an Application. Resolvers match the registered context type exactly and are selected by @Produces specificity; multiple matching resolvers are tried until one returns a context.

restHandler(resource)
    .addCustomWriter(providers -> new MyJsonWriter(providers));

You can also pass a factory to addCustomReader or addCustomWriter, as above. The factory receives the handler's Providers instance, which you can keep and use when reading or writing. Lookups are available after build() completes. Provider fields are not injected.

A resource method's @Context Application parameter receives the application used to create the handler. If you use RestHandlerBuilder.fromApplication(application), this is the same object you passed to the builder. If you register resources and providers directly on the builder, Mu Server supplies an Application containing an immutable snapshot of those resources and custom JAX-RS providers.

WebJar resources

To serve files from a WebJar, add its dependency to your application and use webjarHandler. For example, with the Swagger UI WebJar dependency:

ContextHandlerBuilder.context("/swagger-ui")
    .addHandler(ResourceHandlerBuilder.webjarHandler("swagger-ui"));

The one-argument helper reads the version from Maven metadata under org.webjars or org.webjars.npm. To select a version explicitly, pass the version from your WebJar dependency as the second argument: webjarHandler("swagger-ui", swaggerUiVersion). This is useful when metadata is absent or multiple versions are present. Resource URLs are relative to the selected WebJar's version directory.

Array parameters

You can now use an array to receive repeated parameter values:

@GET
public List<Result> find(@QueryParam("tag") String[] tags) {
    return searchForAll(tags);
}

A request such as ?tag=java&tag=http supplies both values without requiring a collection parameter. The same support applies to the parameter annotations and multipart upload arrays described above.

XML entities

Resources can now read and write XML using javax.xml.transform.Source. For example:

@POST
@Consumes("application/xml")
@Produces("application/*+xml")
public Source echo(Source document) {
    return document;
}

Mu Server 3 includes the standard Jakarta REST entity provider for javax.xml.transform.Source. It reads Source and StreamSource, and writes any Source implementation, for text/xml, application/xml, and structured suffix types such as application/vnd.example+xml. Declared XML charsets and byte-order marks are honoured. When writing, secure processing is enabled and external DTD and stylesheet access are disabled.

The request entity stream remains open through response serialisation. A resource can return its incoming Source, InputStream, or Reader, including when a request filter wraps the input in a GZIPInputStream. Mu closes the request entity stream after writing the response.

Jakarta REST applications

To create a handler from a Jakarta REST Application, use RestHandlerBuilder.fromApplication:

Application application = new MyApplication();

MuServer server = MuServerBuilder.httpServer()
    .addHandler(RestHandlerBuilder.fromApplication(application))
    .start();

Instances returned by Application.getSingletons() are registered as singleton resources or providers. Provider classes returned by getClasses() are constructed once using a public no-argument constructor.

Resource classes in getClasses() are rejected because Jakarta REST gives them a per-request lifecycle, while Mu Server supports singleton resource instances. Application properties, features, dynamic features, and classpath scanning are not supported. ContextResolver registration is supported. Components must be safe for concurrent use. fromApplication rejects applications annotated with @ApplicationPath; use SeBootstrap for those applications.

Java SE bootstrap

You can also start an application with the Jakarta REST 3.1 SeBootstrap API:

MuRuntimeDelegate.ensureSet();

SeBootstrap.Configuration configuration = SeBootstrap.Configuration.builder()
    .port(SeBootstrap.Configuration.FREE_PORT)
    .rootPath("/service")
    .build();

CompletionStage<SeBootstrap.Instance> started =
    SeBootstrap.start(new MyApplication(), configuration);

SeBootstrap supports HTTP and HTTPS, dynamic ports, configured root paths, @ApplicationPath, and a shutdown method returning CompletionStage. The current implementation performs shutdown before returning that stage. Mu Server does not install its RuntimeDelegate globally merely by appearing on the classpath. Before using SeBootstrap, either call MuRuntimeDelegate.ensureSet(), as above, or select it with the Jakarta REST system property when no service-loaded delegate takes precedence:

-Djakarta.ws.rs.ext.RuntimeDelegate=io.muserver.rest.MuRuntimeDelegate

HTTPS bootstrap supports NONE, OPTIONAL, and MANDATORY client-certificate authentication:

SeBootstrap.Configuration configuration = SeBootstrap.Configuration.builder()
    .protocol("HTTPS")
    .sslContext(serverSslContext)
    .sslClientAuthentication(
        SeBootstrap.Configuration.SSLClientAuthentication.MANDATORY)
    .build();

The same modes are available when configuring Mu Server directly:

new HttpsConfigBuilder()
    .withClientCertificateTrustManager(clientTrustManager)
    .withClientCertificateAuthentication(
        ClientCertificateAuthentication.MANDATORY);

OPTIONAL accepts clients without a certificate but validates any certificate they provide; MANDATORY requires a trusted certificate at the TLS handshake. Existing direct configurations that set a client-certificate trust manager without choosing a mode remain optional.

Server Sent Events

Feedback

If you find a regression or a Jakarta REST feature that does not behave as expected, please raise an issue. You can also compare the 2.4.3 and 3.x source trees.