Notes on the Migration from Quarkus RESTEasy Classic to RESTEasy Reactive
Overview
From RESTEasy Classic to RESTEasy Reactive
Until Quarkus 2.8 RESTEasy Classic was the default JAX-RS implementation when using REST in a Quarkus project. Then they switched over to RESTEasy Reactive, which is the reactive version but also contains the classic blocking flavor. Additionally, the quarkus-resteasy-mutiny extension was set deprecated since its functionality is already contained in RESTEasy Reactive. This produces a friendly reminder everytime your run Quarkus:
1WARN [io.quarkus.resteasy.mutiny.deployment.ResteasyMutinyProcessor] (build-27) {} - The quarkus-resteasy-mutiny extension is deprecated. Switch to RESTEasy Reactive instead.
2This extension adds support for Uni and Multi to RESTEasy Classic, without using the reactive execution model, as RESTEasy Classic does not use it. To properly integrate Mutiny and RESTEasy, use RESTEasy Reactive. See https://quarkus.io/guides/getting-started-reactive for detailed instructions
Since both, RESTEasy Classic and the quarkus-resteasy-mutiny
extension, were used on my current project and since it is never
a good idea to use deprecated code because eventually that code will be removed, I grabbed the task of migrating the code to RESTEasy Reactive.
Initially it is a good idea to start with the mentioned official migration guide giving important hints on how to switch from RESTEasy Classic to Reactive. Apart from adjusting the dependencies in the pom.xml
probably the most important task is to adjust the mentioned annotations from the old org.jboss.
ones to the new org.jboss.
ones.
Naturally the guide cannot contain every obstacle one might face during a migration, hence I decided to write down some additional remarks on problems I faced and possible solutions.
Of course the concrete migration scenario depends heavily on the individual code base.
After having worked through the migration guide and having applied everything relevant, I ended up with quite some compilation issues.
Compilation Issues After the Migration
Missing Exception Class
The exception ResteasyWebApplicationException
was used in the code base in the context of REST client calls to other services. It was imported from quarkus-resteasy-mutiny
, so for obvious reasons removing the dependency caused compilation errors. Searching the internet didn't really help finding an appropriate replacement, so I decided to replace it for now with
1org.jboss.resteasy.reactive.ClientWebApplicationException
and everything seems to work with this one as expected.
Handling Multipart Form Data
The application provides some REST endpoints for handling content of type multipart/form-data
. For receiving the data from the POST
MultipartFormDataInput
was defined and in the further processing, the parts were extracted into InputPart
variables.
Both interfaces don't exist in RESTEasy Reactive, so something else had to replace them.
The multipart section in the Quarkus RESTEasy Reactive guide helped to solve this issue and I replaced the endpoint method parameter with corresponding @RestForm
annotated parameters as described there, so
1@POST
2@Path("/receive")
3@Consumes(MediaType.MULTIPART_FORM_DATA)
4@Produces(MediaType.APPLICATION_JSON)
5public Response receive(final MultipartFormDataInput inputData) {
6 // logic for extracting InputPart objects
7 // ...
8}
became
1@POST
2@Path("/receive")
3@Consumes(MediaType.MULTIPART_FORM_DATA)
4@Produces(MediaType.APPLICATION_JSON)
5public Response receive(final @RestForm String filename, final @RestForm FileUpload file) {
6 // ...
7}
With these changes the code became much shorter because the whole extraction logic was not necessary anymore because it is handled by Quarkus itself. After these adaptions the compilation finally succeeded, so it was time to run the tests and start up the application.
Runtime Issues After the Migration
Changed Behaviour of Optional<List<>>
REST Parameters
When you define a REST endpoint in RESTEasy Classic like for example
1@Path("/plants")
2@GET
3public List<Plant> getPlants(@QueryParam("name") final Optional<List<String>> names) {
4 // ...
5}
and the query parameter name
is not set in the request, then you receive an Optional
with an empty list in the method parameter names
. This behavior has changed with RESTEasy Reactive in the way that you now receive an empty Optional
when the name
parameter is not provided. From my point view this is a reasonable change because it's more what you would expect in that case. However, it can be a breaking change like it was in my case.
This change might also occur for other types of Optionals, I haven't tested that.
Blocking Exception on Endpoints
Some tests failed because a BlockingNotAllowedException
was thrown with the following message:
1A blocking operation occurred on the IO thread. This likely means you need to annotate <signature-of-the-method> with @io.smallrye.common.annotation.Blocking. Alternatively you can annotate the class <name-of-the-affected-resource> to make every method on the class blocking, or annotate your sub class of the javax.ws.rs.core.Application class to make the whole application blocking
This means that a synchronous (blocking) operation was executed within an endpoint that is considered asynchronous by Quarkus and hence executed on the I/O thread and not in a worker thread. Since the I/O thread should not be blocked by long-running tasks because it can cause performance issues, it's good that Quarkus throws an exception here and not ignores it silently. There is a very good blog post that explains the details and also in which cases an endpoint is considered synchronous or asynchronous respectively.
Further investigations uncovered that indeed the underlying service called another service synchronously. Because refactoring this bad behaving service was too complex and not in scope of the task, I decided to annotate the affected resource methods for now with @Blocking
which leads to the annotated methods being executed in a worker thread instead of the I/O thread. The effect on performance in this case is negligible because these endpoints are rarely called.
Incorrect Path Matching
This problem cost me a couple of hours to understand what the actual problem was. Some integration tests for the REST endpoints failed with strange HTTP status codes. It turned out that apparently the way URLs are matched onto methods in resources has changed in RESTEasy Reactive. For example when you have defined two endpoints GET /my/{id}
and GET /my/rest/path
the behavior with RESTEasy Classic was as follows:
Call | Matched Endpoint | HTTP Status |
---|---|---|
GET /my/rest |
GET /my/{id} |
200 |
GET /my/rest/path |
GET /my/rest/path |
200 |
Maybe it's not the best way to design an API, but if you have endpoints like these defined, this behavior looks reasonable.
However, with RESTEasy Reactive this changed to:
Call | Matched Endpoint | HTTP Status |
---|---|---|
GET /my/rest |
GET /my/{id} |
200 |
GET /my/rest/path |
- |
404 |
It seems RESTEasy Reactive cannot correctly handle these overlapping paths and hence returns an HTTP 404
for the second case. There are at least two similar but not identical GitHub issues regarding this problem: #27154 and #30667. It seems that they are already working on a fix. With the most recent Quarkus version (as of writing this 2.16.0.Final) the problem still exists.
In my concrete case it turned out that one of these two endpoints was not used anymore and I could simply remove it to resolve the problem, but this is not the default case of course. So basically you have only two options: Adjust your API or wait with the migration until the Quarkus team has released a fix.
Injection of REST Clients via Provider
After all tests were green, running the application led to Quarkus complaining about not being able to inject some REST clients.
1$ mvn quarkus:dev
2
3ERROR [io.quarkus.runner.bootstrap.StartupActionImpl] (Quarkus Main Thread) {} - Error running Quarkus: java.lang.reflect.InvocationTargetException
4(...)
5Caused by: java.lang.ExceptionInInitializerError
6(...)
7Caused by: java.lang.RuntimeException: Failed to start quarkus
8(...)
9Caused by: java.lang.RuntimeException: The Rest Client configuration cannot be initialized at this stage. Try to wrap your Rest Client injection in the Provider<> interface:
10
11 @Inject
12 @RestClient
13 Provider<MyRestClientInterface> myRestClient;
14
15(...)
Very fair of the Quarkus team to also include the solution for this issue into the error message, so changing the code from
1@Inject
2@RestClient
3MyRestClientInterface myRestClient;
4
5public void callOtherSystem() {
6 myRestClient.doRestCall();
7 // ...
8}
to
1import javax.inject.Provider;
2
3@Inject
4@RestClient
5Provider<MyRestClientInterface> myRestClient;
6
7public void callOtherSystem() {
8 myRestClient.get().doRestCall();
9 // ...
10}
solved also this problem.
Problem with retrieving the targeted resource
One more thing popped up while testing on the development environment. There is a specific ContainerRequestFilter
that extracted the targeted resource like
1@Provider
2public class MyRequestFilter implements ContainerRequestFilter {
3
4 @Override
5 public void filter(final ContainerRequestContext requestContext) {
6 final List<Object> matchedResources = requestContext.getUriInfo().getMatchedResources();
7 // ...
8 }
9}
and then acts depending on which resource the request is aimed at. However, the returned list was always empty when using RESTEasy Reactive, hence the filter did not work as expected. Since this filter is not a pre-matching filter, I currently see no reason for this behavior.
Fortunately Quarkus permits the injection of a ResourceInfo
object which also allows retrieving this information:
1import javax.ws.rs.container.ResourceInfo;
2
3@Provider
4@RequiredArgsConstructor
5public class MyRequestFilter implements ContainerRequestFilter {
6 private final ResourceInfo resourceInfo;
7
8 @Override
9 public void filter(final ContainerRequestContext requestContext) {
10 final Class<?> clazz = resourceInfo.getResourceClass();
11
12 if (clazz == MyClass.class) {
13 // ...
14 }
15 }
16}
Conclusion
After these adaptions with one exception the application is running now smooth with the new RESTEasy Reactive under the hood.