Mu Server 3 preview

Mu Server 3 is ready for early testing.

The current prerelease is 0.0.3.2. Try it with an existing 2.x application and report compatibility problems or unexpected behaviour on the issue tracker.

These notes apply to applications upgrading from any Mu Server 2.x release. The source comparison covers changes made after 2.3.2 through the master branch used to build 0.0.3.2.

The headlines

Jakarta REST 3.1 limitation: Mu Server 3 does not support the new jakarta.ws.rs.core.EntityPart multipart API introduced in Jakarta REST 3.1.

What you need to change

The Maven coordinates and Java package names are unchanged. To try the Mu Server 3 prerelease, update the version as shown below. This is an early release intended for compatibility testing and may contain regressions, so assess the risk carefully before using it in production.

<dependency>
    <groupId>io.muserver</groupId>
    <artifactId>mu-server</artifactId>
    <version>0.0.3.2</version>
</dependency>
Upgrade requirement Mu Server 2 Mu Server 3
Java Java 8 or later Java 11 or later
Jakarta REST API Jakarta REST 3.0 Jakarta REST 3.1
Logging API SLF4J 1.7 SLF4J 2.0. Use an SLF4J 2.x provider; SLF4J 1.7 bindings are not discovered by SLF4J 2's service-provider mechanism.
Nullness metadata No API-wide nullness annotation contract JSpecify annotations across the public APIs. JVM method descriptors are unchanged, but strict static analysis and Kotlin may now identify unsafe calls.

For completeness, the unused public io.muserver.Toggles class has been removed. It was not used by Mu Server itself and is not expected to affect applications.

Why Jakarta REST compliance matters

The goal of the compliance work is predictable, portable behaviour. Resource classes, providers, filters, and interceptors written to the Jakarta REST specification should produce fewer surprises when moved between implementations, including in edge cases where Mu Server 2.x had its own behaviour. Mu Server 3 passes all applicable tests in the Jakarta REST 3.1 Technology Compatibility Kit (TCK); tests for deliberately unsupported features, such as EntityPart, are excluded.

The main change is much closer compliance with Jakarta REST 3.1. A typical application with unambiguous resource paths and one JSON provider may need no source changes beyond the Java, Jakarta REST, and logging upgrades. Applications using custom providers, filters, interceptors, converters, generic resource interfaces, or SSE should check the cases below.

Behaviour changes to test

REST exception responses

Mu Server 3 registers an exception mapper for Throwable by default. Application-specific exception mappers still take precedence, and a WebApplicationException that already has an entity keeps its original response.

Change Mu Server 2 Mu Server 3
Unhandled resource exception
A resource throws new IllegalStateException("database path: /secret").
500 Internal Server Error using Mu Server's standard HTML error page. 500 Internal Server Error with application/problem+json, Cache-Control: no-store, a unique urn:uuid: instance, and the generic detail An unexpected error occurred. The exception and instance ID are logged, but the exception message is not exposed to the client.
Malformed Accept header
Send Accept: text.
400 Bad Request using the standard error response. 400 Bad Request with application/problem+json.

To restore Mu Server's HTML fallback for otherwise-unmapped exceptions, remove the default mapper:

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

To change details such as whether 4xx instance IDs are logged, replace it with a configured mapper. By default, 5xx instance IDs are logged and 4xx instance IDs are not.

restHandler(resource)
    .addExceptionMapper(
        Throwable.class,
        ProblemDetailsExceptionMapperBuilder.problemDetailsExceptionMapper()
            .withLog4xxProblemDetailsInstanceIds(true)
            .build());

Graceful shutdown

Change Mu Server 2 Mu Server 3
MuServer.stop(...) result
Gracefully stop a server with an in-flight request, both when the request completes within the timeout and when it does not.
Returns false after a clean shutdown and true after timing out. Returns true after a clean shutdown and false when requests do not finish, matching the documented contract.

Resource matching, parameters, and locations

Change Mu Server 2 Mu Server 3
Invalid URI parameter values
Given find(@QueryParam("colour") Colour colour), request ?colour=purple.
400 Bad Request 400 Bad Request with application/problem+json. If the default Throwable mapper is removed, the underlying Jakarta REST result is 404 Not Found.
Array-valued resource parameters
Given find(@QueryParam("tag") String[] tags), request ?tag=one&tag=two.
Object-array parameters are not supported. tags is ["one", "two"].
Non-public resource methods
Annotate package-private @GET String hidden().
The method is silently ignored. The method is still ignored, as Jakarta REST exposes public resource methods only, but a startup warning identifies the problem.
Relative Location values
From /app/items/123, return Response.created(URI.create("next")).
Location: /app/items/next Location: /app/next
Requests with no body
Send a bodyless request to a resource method with @Consumes("application/json").
The resource method may not match. The resource method can match; the absent content type is treated as a wildcard.
Repeated path captures
Match a path template that captures the same parameter more than once and inject it into a collection.
Only one captured value may be retained. All captured values are retained; the capture count also participates in resource ranking.
Empty path captures and defaults
Inject an empty path capture into a parameter with @DefaultValue.
An empty capture can be replaced by its @DefaultValue. An empty capture stays empty; only an absent value uses @DefaultValue.
Inherited resource declarations
Declare a resource annotation or generic entity type on a parent class or interface.
The annotation may be missed or the generic type resolved as Object. The annotation and concrete generic type are retained.

Object arrays are supported for @QueryParam, @HeaderParam, @MatrixParam, @FormParam, and @CookieParam. Repeated values become array elements, a missing parameter produces an empty array, and @DefaultValue produces a one-element array. Element conversion uses the same built-in or application ParamConverter as scalar parameters. UploadedFile[] is also supported for multipart uploads.

@PathParam arrays and primitive arrays such as int[] are not supported. Cookie arrays use jakarta.ws.rs.core.Cookie[], not io.muserver.Cookie[].

For URI parameter conversion failures, including unknown enum values, the default mapper preserves the 400 status and descriptive error used in 2.x. The JSON includes parameter and suppliedValue; for a standard enum it also includes allowedValues.

For a bespoke response, Mu Server 3 exposes UriParameterConversionException with the parameter name, supplied value, target type, allowed values when known, and original cause, so it can be mapped directly.

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.

Change Mu Server 2 Mu Server 3
Writing application/x-www-form-urlencoded
Write the values blue green and blue+green.
blue%20green and blue%2Bgreen blue+green and blue%2Bgreen
@Encoded @FormParam
Inject q=blue+green into @Encoded @FormParam("q") String q.
blue%20green blue+green
Reading a plus sign from a path
Call decoded UriInfo path access for /cars/blue+green.
cars/blue green cars/blue+green
Reading matrix parameters
Read ;colour=blue+green%20car.
colour=blue green car colour=blue+green car
Building URI components
Add the values blue green and blue+green as path or query components.
Component-specific encoding is not applied consistently. blue%20green and blue%2Bgreen
UriInfo.getAbsolutePath()
Get the absolute path when the request authority differs from the configured base, or when the path contains an encoded slash such as %2F.
The base authority may be used and %2F may be treated as a path delimiter. The request authority and encoded %2F are preserved.

Providers, filters, and interceptors

Change Mu Server 2 Mu Server 3
Application providers versus built-ins
Register an application provider for Object, then read or write a String.
The built-in String provider can be selected. The application provider wins, even though its accepted Java type is broader.
Reader selection by media type
Register compatible readers for */* and application/json, then receive application/json.
The */* reader can be selected. The compatible reader with the more specific media type is preferred.
Writer selection by Java type
Return a Dog from a method declared as Object, with writers for Animal and Object.
The Object writer can be selected. The Animal writer wins because it is nearest to the runtime Dog type.
Generic entity types
Return a generic entity through an inherited method, CompletionStage, or GenericEntity.
The provider may receive a raw type or Object. The declared generic type is retained for provider selection.
Request entity-stream filters
In a request filter, replace the entity stream with a GZIPInputStream and remove the stale Content-Encoding and Content-Length headers.
Gzip decoding can fail or produce a truncated request entity. The resource receives the complete decompressed entity and the removed headers remain absent.
Response entity-stream filters
In a response filter, wrap the entity stream with GZIPOutputStream, then set Content-Encoding: gzip and other response metadata.
Creating the wrapper can commit the response before the filter's status and header changes take effect. The client receives the changed status and headers with a valid gzip body.
Reader and writer interceptor order
Register interceptors with priorities 100 and 200.
Order can depend on registration; a reader interceptor added second can run first. Priority 100 runs before priority 200.
abortWith(...) in a request filter
Call abortWith(response) before later request filters.
Later request filters can still run, and an exception mapper can replace the supplied response. The request filter chain stops and the supplied response bypasses exception mapping.
Non-matching name-bound interceptors
Put a non-matching name-bound interceptor before another applicable interceptor.
Skipping one can accidentally stop the remaining interceptor chain. The non-matching interceptor is skipped and the rest of the chain continues.
WebApplicationException with an entity
Throw a WebApplicationException that already contains a response entity while an exception mapper is registered.
An exception mapper can replace the response already carried by the exception. The response and its entity are preserved.
Mutating request state from a response filter
Call abortWith or setEntityStream from a response filter.
The call may be accepted after request processing has finished. They throw IllegalStateException.
Headers changed by filters or interceptors
Change a response header before writing, or set an application-supplied Date.
The change may miss the response; setting Date can produce two values. The change is sent; an application-supplied Date replaces the server default.

Responses, cookies, and variants

Change Mu Server 2 Mu Server 3
Default ResponseBuilder status
Build a response without explicitly setting its status.
The builder does not derive a status from the presence of an entity. A response with an entity defaults to 200 OK; one without defaults to 204 No Content.
Session cookies
Return a NewCookie with its default maximum age.
The resulting Set-Cookie can expire immediately. Session cookies omit both Max-Age and Expires.
SameSite cookies
Parse or serialize a Jakarta REST 3.1 NewCookie with SameSite.
Jakarta REST 3.1 SameSite values are not supported. Strict, Lax, and None are parsed and serialized. For duplicate attributes the last value wins; invalid values are rejected.
Cookie comments
Serialize a NewCookie with comment A "quoted" \ comment.
The Comment attribute is omitted. The attribute is emitted as Comment="A \"quoted\" \\ comment". Carriage returns and line feeds are rejected.
Unknown response-header object types
Add an object with no registered RuntimeDelegate.HeaderDelegate as a response header.
Mu Server falls back to toString(), but asking the runtime delegate directly throws a Mu Server exception. The response still uses toString(), while RuntimeDelegate.createHeaderDelegate(...) returns null as required by Jakarta REST.
Cookies on wrapped responses
Add a cookie to a wrapped Jakarta REST response.
The same Set-Cookie value can be emitted twice. Each cookie is emitted once.
Response entities backed by Reader
Return a Reader as the response entity.
A Reader response entity is not handled as a standard entity type. Reader entities are streamed to the response.
String response-header view
Save the map returned by Response.getStringHeaders(), then mutate either it or Response.getHeaders().
The saved string map is a snapshot and can become stale. Both maps are live views: changes through either one are immediately visible through the other.
Variant selection
Select from variants containing compatible media types or wildcard encodings, or pass a null variant list.
Null lists, wildcard encodings, or compatible media types can produce incorrect selection. Null variant lists are rejected and compatible media types and wildcard encodings are ranked correctly.

New APIs and capabilities

Use arrays for 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.

Read and write XML Source entities

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

Start from a Jakarta REST Application

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, context resolvers, and classpath scanning are not supported.

Use Jakarta REST 3.1 Java SE bootstrap

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 asynchronous shutdown. 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 standard Jakarta REST system property:

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

SSE lifecycle and concurrency

Change Mu Server 2 Mu Server 3
Closed state
Close an SSE sink or Jakarta REST response, then inspect its state or send another event.
SseEventSink.isClosed() and Response.isClosed() do not report it correctly. Both report their closed state; sending to a closed SSE sink throws IllegalStateException.
Broadcaster shutdown
Call SseBroadcaster.close(boolean cascading).
The cascading flag is not fully honoured. Cascading close shuts the sinks; non-cascading close leaves them open.
Concurrent callbacks
Close, disconnect, broadcast, or shut down a broadcaster concurrently.
A close or error callback can be delivered more than once. Each registration receives its close or error callback at most once.

Try it with your 2.x application

The cases above are deliberately detailed: many came from Jakarta REST compatibility tests and are unlikely to affect a straightforward resource. They are the places most worth exercising if your application depends on overlapping providers, filter order, custom conversion, generic resource interfaces, raw URI values, cookies, or SSE.

Please report a regression—or a Jakarta REST behaviour you expect but do not see—on the issue tracker. You can review the complete source history from 2.3.2 to the current master branch.