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
- The minimum Java version changes from 8 to 11.
- Jakarta REST is upgraded from 3.0 to 3.1 and passes its TCK for the features Mu Server supports.
- Public APIs now have JSpecify nullness annotations.
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 headerSend 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(...) resultGracefully 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 valuesFrom /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-urlencodedWrite the values blue green and blue+green.
|
blue%20green and blue%2Bgreen |
blue+green and blue%2Bgreen
|
@Encoded @FormParamInject 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 filterCall 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 entityThrow 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 statusBuild 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 cookiesParse 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 ReaderReturn 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.