Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support blocking exception mappers in REST Client Reactive #32644

Merged
merged 1 commit into from
Apr 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
62 changes: 62 additions & 0 deletions docs/src/main/asciidoc/rest-client-reactive.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,68 @@ Naturally this handling is per REST Client. `@ClientExceptionMapper` uses the de

NOTE: Methods annotated with `@ClientExceptionMapper` can also take a `java.lang.reflect.Method` parameter which is useful if the exception mapping code needs to know the REST Client method that was invoked and caused the exception mapping code to engage.

=== Using @Blocking annotation in exception mappers

In cases that warrant using `InputStream` as the return type of REST Client method (such as when large amounts of data need to be read):

[source, java]
----
@Path("/echo")
@RegisterRestClient
public interface EchoClient {

@GET
InputStream get();
}
----

This will work as expected, but if you try to read this InputStream object in a custom exception mapper, you will receive a `BlockingNotAllowedException` exception. This is because `ResponseExceptionMapper` classes are run on the Event Loop thread executor by default - which does not allow to perform IO operations.

To make your exception mapper blocking, you can annotate the exception mapper with the `@Blocking` annotation:

[source, java]
----
@Provider
@Blocking <1>
public class MyResponseExceptionMapper implements ResponseExceptionMapper<RuntimeException> {

@Override
public RuntimeException toThrowable(Response response) {
if (response.getStatus() == 500) {
response.readEntity(String.class); <2>
return new RuntimeException("The remote service responded with HTTP 500");
}
return null;
}
}
----

<1> With the `@Blocking` annotation, the MyResponseExceptionMapper exception mapper will be executed in the worker thread pool.
<2> Reading the entity is now allowed because we're executing the mapper on the worker thread pool.

Note that you can also use the `@Blocking` annotation when using @ClientExceptionMapper:

[source, java]
----
@Path("/echo")
@RegisterRestClient
public interface EchoClient {

@GET
InputStream get();

@ClientExceptionMapper
@Blocking
static RuntimeException toException(Response response) {
if (response.getStatus() == 500) {
response.readEntity(String.class);
return new RuntimeException("The remote service responded with HTTP 500");
}
return null;
}
}
----

[[multipart]]
== Multipart Form support

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,21 +90,14 @@ GeneratedClassResult generateResponseExceptionMapper(AnnotationInstance instance
throw new IllegalStateException(message);
}

StringBuilder sigBuilder = new StringBuilder();
sigBuilder.append(targetMethod.name()).append("_").append(targetMethod.returnType().name().toString());
for (Type i : targetMethod.parameterTypes()) {
sigBuilder.append(i.name().toString());
}

int priority = Priorities.USER;
AnnotationValue priorityAnnotationValue = instance.value("priority");
if (priorityAnnotationValue != null) {
priority = priorityAnnotationValue.asInt();
}

ClassInfo restClientInterfaceClassInfo = targetMethod.declaringClass();
String generatedClassName = restClientInterfaceClassInfo.name().toString() + "_" + targetMethod.name() + "_"
+ "ResponseExceptionMapper" + "_" + HashUtil.sha1(sigBuilder.toString());
String generatedClassName = getGeneratedClassName(targetMethod);
try (ClassCreator cc = ClassCreator.builder().classOutput(classOutput).className(generatedClassName)
.interfaces(ResteasyReactiveResponseExceptionMapper.class).build()) {
MethodCreator toThrowable = cc.getMethodCreator("toThrowable", Throwable.class, Response.class,
Expand Down Expand Up @@ -143,6 +136,17 @@ GeneratedClassResult generateResponseExceptionMapper(AnnotationInstance instance
return new GeneratedClassResult(restClientInterfaceClassInfo.name().toString(), generatedClassName, priority);
}

public static String getGeneratedClassName(MethodInfo methodInfo) {
StringBuilder sigBuilder = new StringBuilder();
sigBuilder.append(methodInfo.name()).append("_").append(methodInfo.returnType().name().toString());
for (Type i : methodInfo.parameterTypes()) {
sigBuilder.append(i.name().toString());
}

return methodInfo.declaringClass().name().toString() + "_" + methodInfo.name() + "_"
+ "ResponseExceptionMapper" + "_" + HashUtil.sha1(sigBuilder.toString());
}

private static boolean ignoreAnnotation(MethodInfo methodInfo) {
// ignore the annotation if it's placed on a Kotlin companion class
// this is not a problem since the Kotlin compiler will also place the annotation the static method interface method
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.annotation.RegisterProviders;
import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper;
import org.jboss.jandex.DotName;

import io.quarkus.rest.client.reactive.ClientExceptionMapper;
Expand All @@ -32,6 +33,8 @@ public class DotNames {
public static final DotName CLIENT_EXCEPTION_MAPPER = DotName.createSimple(ClientExceptionMapper.class.getName());
public static final DotName CLIENT_REDIRECT_HANDLER = DotName.createSimple(ClientRedirectHandler.class.getName());

public static final DotName RESPONSE_EXCEPTION_MAPPER = DotName.createSimple(ResponseExceptionMapper.class.getName());

static final DotName METHOD = DotName.createSimple(Method.class.getName());

private DotNames() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@
import static io.quarkus.rest.client.reactive.deployment.DotNames.REGISTER_CLIENT_HEADERS;
import static io.quarkus.rest.client.reactive.deployment.DotNames.REGISTER_PROVIDER;
import static io.quarkus.rest.client.reactive.deployment.DotNames.REGISTER_PROVIDERS;
import static io.quarkus.rest.client.reactive.deployment.DotNames.RESPONSE_EXCEPTION_MAPPER;
import static java.util.Arrays.asList;
import static java.util.stream.Collectors.*;
import static org.jboss.resteasy.reactive.common.processor.EndpointIndexer.CDI_WRAPPER_SUFFIX;
import static org.jboss.resteasy.reactive.common.processor.JandexUtil.isImplementorOf;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.BLOCKING;
import static org.jboss.resteasy.reactive.common.processor.scanning.ResteasyReactiveScanner.BUILTIN_HTTP_ANNOTATIONS_TO_METHOD;

import java.lang.annotation.RetentionPolicy;
Expand Down Expand Up @@ -497,6 +500,24 @@ void addRestClientBeans(Capabilities capabilities,
}
}
}

Set<String> blockingClassNames = new HashSet<>();
Set<AnnotationInstance> registerBlockingClasses = new HashSet<>(index.getAnnotations(BLOCKING));
for (AnnotationInstance registerBlockingClass : registerBlockingClasses) {
AnnotationTarget target = registerBlockingClass.target();
if (target.kind() == AnnotationTarget.Kind.CLASS
&& isImplementorOf(index, target.asClass(), RESPONSE_EXCEPTION_MAPPER)) {
// Watch for @Blocking annotations in classes that implements ResponseExceptionMapper:
blockingClassNames.add(target.asClass().toString());
} else if (target.kind() == AnnotationTarget.Kind.METHOD
&& target.asMethod().annotation(CLIENT_EXCEPTION_MAPPER) != null) {
// Watch for @Blocking annotations in methods that are also annotated with @ClientExceptionMapper:
blockingClassNames.add(ClientExceptionMapperHandler.getGeneratedClassName(target.asMethod()));
}
}

recorder.setBlockingClassNames(blockingClassNames);

if (LaunchMode.current() == LaunchMode.DEVELOPMENT) {
recorder.setConfigKeys(configKeys);
}
Expand Down