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
- Java 11 or later is now required, up from Java 8.
- HTTP
QUERYrequests are supported in Mu handlers and Jakarta REST resources usingio.muserver.rest.QUERY. - Generated OpenAPI documents are upgraded from 3.0 to 3.2.1, including
QUERYoperations and schemas for server-sent events (SSE). - Jakarta REST is upgraded from 3.0 to 3.1. If you manage the Jakarta REST API dependency separately, update it to 3.1.
- SLF4J is upgraded from 1.7 to 2.0. You will need an SLF4J 2.x logging provider, as the old 1.7 bindings are not discovered by SLF4J 2's service-provider mechanism. Check that your application still logs on startup.
- Public APIs now have JSpecify nullness annotations. If you use Kotlin or a null-checking build, you may need to fix calls that pass null where it is not allowed. These annotations do not change JVM method descriptors.
- The unused public
io.muserver.Togglesclass has been removed. Mu Server itself did not use it, so this is not expected to affect applications.
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:
- Fixed a route-matching bug for resource classes with an empty or slash-only class-level path
(
@Path("")or@Path("/")). On a class with one of these annotations, a@GET @Path("children/{id}")method now handles/children/123. In 2.x, the class-level path failed to match the request, resulting in404before the method was reached. -
JAX-RS requires
404 Not Foundfor parameter conversion failures such as passing?colour=purpletofind(@QueryParam("colour") Colour colour)whenpurpleis not a valid enum value. Mu Server 3 now follows this rule internally. However, the exception mapper enabled by default converts these errors to400 Bad Request, preserving the 2.x status code, with anapplication/problem+jsonresponse body.This status change therefore affects you if you remove the default
Throwablemapper or use a custom exception mapper to handle these errors. The underlying exception now carries a404rather than a400, so check which status your mapper returns.The default mapper also preserves the descriptive error used in 2.x. The JSON includes
parameterandsuppliedValue; for a standard enum it also includesallowedValues. To return your own error response, add a mapper forUriParameterConversionException. It provides the parameter name, supplied value, target type, allowed values when known, and original cause. -
Object-array parameters are now supported. For example,
find(@QueryParam("tag") String[] tags)receives["one", "two"]for?tag=one&tag=two.This applies to
@QueryParam,@HeaderParam,@MatrixParam,@FormParam, and@CookieParam. Repeated values become array elements, a missing parameter produces an empty array, and@DefaultValueproduces a one-element array. Element conversion uses the same built-in or applicationParamConverteras scalar parameters.UploadedFile[]is also supported for multipart uploads.@PathParamarrays and primitive arrays such asint[]are not supported. Cookie arrays usejakarta.ws.rs.core.Cookie[], notio.muserver.Cookie[]. - Collection-valued
@QueryParamand@HeaderParamparameters no longer require an explicit collection strategy at startup. The default isCollectionParameterStrategy.NO_TRANSFORM:?tag=one,twosupplies one value, while?tag=one&tag=twosupplies two. An explicitSPLIT_ON_COMMAsetting continues to work and now consistently removes empty split values. - Non-public resource methods, such as a package-private
@GET String hidden(), now produce a startup warning. They are still ignored, as Jakarta REST only exposes public resource methods. - Relative
Locationvalues are resolved against the base URI. For example, returningResponse.created(URI.create("next"))from/app/items/123now givesLocation: /app/next, where 2.x gaveLocation: /app/items/next. This may cause your REST handlers to return incorrect URLs in responses if they use relativeLocationvalues and rely on the 2.x behaviour. - A request without
Content-Typenow uses a wildcard when matching resource methods, whether or not it has a body. In 2.x it could fail to match a method with@Consumes("application/json"). Entity readers still useapplication/octet-streamwhen the header is absent, so send an explicit content type if the reader needs one. - When a path template captures the same parameter more than once, all values are retained for collection parameters. Previously only one value might be kept. The number of captures also counts when ranking matching resources.
- An empty path capture now stays empty, even if the parameter has a
@DefaultValue. For example, with@Path("/{param:.*}")and a method parameter declared as@DefaultValue("DEFAULT") @PathParam("param") String param, a request to/now supplies"". The.*pattern matches an empty string, so the value is present but empty. In 2.x it could be replaced with"DEFAULT"; now only an absent value uses the default. - Resource annotations and concrete generic entity types declared on parent classes or interfaces are now
retained. Previously an annotation could be missed, or a generic type resolved as
Object.
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:
- Readers with a more specific compatible media type are preferred. For an
application/jsonrequest, a compatibleapplication/jsonreader now wins over a*/*reader. - Writers are selected using the runtime Java type. If a method declared as
Objectreturns aDog, a writer forAnimalnow wins over one forObjectbecause it is closer toDog. In 2.x theObjectwriter could be selected. - Generic types are retained when an entity comes from an inherited method,
CompletionStage, orGenericEntity. Providers previously could receive a raw type orObject.
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:
- Calling
abortWith(response)in a request filter now stops the request filter chain and sends the supplied response without exception mapping. Previously later request filters could still run and an exception mapper could replace the response. - Skipping a name-bound interceptor that does not match the resource no longer stops the remaining interceptors from running.
- A
WebApplicationExceptionwith its own response entity keeps that response, even when an exception mapper is registered. In 2.x the mapper could replace it. - Calling
abortWithorsetEntityStreamfrom a response filter now throwsIllegalStateException. Previously these calls could be accepted after request processing had finished. - Response header changes made by filters or interceptors before writing are now sent correctly.
An application-supplied
Datereplaces the server default instead of producing two values.
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
- If you do not set a status on a
ResponseBuilder, it now defaults to200 OKwhen there is an entity, or204 No Contentwhen there is none. Previously the builder did not choose a status based on the entity. - Fixed session cookies expiring immediately. A
NewCookiewith the default maximum age now omits bothMax-AgeandExpiresfromSet-Cookie. - Added support for parsing and serialising Jakarta REST 3.1
NewCookieSameSitevalues:Strict,Lax, andNone. The last value wins if the attribute appears more than once, and invalid values are rejected. - Cookie comments are now included. For example, a
NewCookiecomment ofA "quoted" \ commentis written asComment="A \"quoted\" \\ comment". Carriage returns and line feeds are rejected. - For header object types without a registered
RuntimeDelegate.HeaderDelegate,RuntimeDelegate.createHeaderDelegate(...)now returnsnullas required by Jakarta REST, instead of throwing a Mu Server exception. Response headers still fall back totoString()for these types. - Fixed duplicate
Set-Cookievalues on wrapped Jakarta REST responses. Each cookie is now sent once. - Added support for streaming a
Readerresponse entity. Response.getStringHeaders()now returns a live view instead of a snapshot. Changes through that map orResponse.getHeaders()are immediately visible through the other.- Fixed variant selection for compatible media types and wildcard encodings. Null variant lists are now rejected.
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
- Fixed
SseEventSink.isClosed()reporting the wrong state. Sending an event to a closed sink now throwsIllegalStateException. Jakarta REST 3.1 also addsResponse.isClosed(), which was not available in 3.0. SseBroadcaster.close(boolean cascading)now honours the cascading flag: a cascading close closes the sinks, while a non-cascading close leaves them open.- Fixed duplicate close and broadcast-failure callbacks when a close, disconnect, broadcast, or broadcaster shutdown happens concurrently. Notifications are tracked per registration. A separate failure while closing a sink can still cause an additional error notification.
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.