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.resteasy.annotations.jaxrs.* ones to the new org.jboss.resteasy.reactive.* 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.

Note

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 body, an endpoint method parameter with type 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.


Feedback Welcome

I am always happy to receive comments, corrections, improvements or feedback in general! Please drop me a note on Mastodon or by E-Mail anytime.