Skip to content

Commit 4424b0c

Browse files
authored
Merge pull request quarkusio#50730 from kevinferrare-1a/feature/unwrap-exception-always
Allow forcing exception unwrapping even when parent type mappers exist
2 parents c04eed2 + 434ca0a commit 4424b0c

File tree

8 files changed

+405
-77
lines changed

8 files changed

+405
-77
lines changed

docs/src/main/asciidoc/rest.adoc

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2422,6 +2422,36 @@ public class Mapper {
24222422
24232423
}
24242424
----
2425+
2426+
By default, `@UnwrapException` only unwraps exceptions when no exception mapper exists for the wrapper exception or its parent types.
2427+
You can control the unwrapping behavior using the `strategy` attribute with one of the following values:
2428+
2429+
* `ExceptionUnwrapStrategy.UNWRAP_IF_NO_MATCH` (default): Unwraps only if neither the thrown exception nor any of its supertypes have a registered mapper. Checks the entire type hierarchy before unwrapping.
2430+
* `ExceptionUnwrapStrategy.UNWRAP_IF_NO_EXACT_MATCH`: Unwraps if the thrown exception type itself has no exact mapper registered. Checks for an exact match first, then unwraps if none is found.
2431+
* `ExceptionUnwrapStrategy.ALWAYS`: Always unwraps exception before checking mappers. Only falls back to checking mappers for the wrapper exception if no mapper is found for any unwrapped cause.
2432+
2433+
[source,java]
2434+
----
2435+
@UnwrapException(value = {WebApplicationException.class}, strategy = ExceptionUnwrapStrategy.UNWRAP_IF_NO_EXACT_MATCH)
2436+
public class Mappers {
2437+
2438+
@ServerExceptionMapper
2439+
public Response handleRuntimeException(RuntimeException ex) {
2440+
// Handles RuntimeException and its subclasses
2441+
return Response.status(599).build();
2442+
}
2443+
2444+
@ServerExceptionMapper
2445+
public Response handleJsonProcessingException(JsonProcessingException ex) {
2446+
// This will be called for JsonProcessingException wrapped in WebApplicationException
2447+
// because UNWRAP_IF_NO_EXACT_MATCH forces unwrapping even though RuntimeException mapper that would match WebApplicationException exists
2448+
return Response.status(400).entity("Invalid JSON").build();
2449+
}
2450+
2451+
}
2452+
----
2453+
2454+
This is useful when you have a general exception mapper for a parent type (like `RuntimeException`) but want to handle specific wrapped exceptions differently.
24252455
====
24262456

24272457
[NOTE]

extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveScanningProcessor.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames;
3535
import org.jboss.resteasy.reactive.common.processor.scanning.ApplicationScanningResult;
3636
import org.jboss.resteasy.reactive.common.processor.scanning.ResteasyReactiveInterceptorScanner;
37+
import org.jboss.resteasy.reactive.server.ExceptionUnwrapStrategy;
3738
import org.jboss.resteasy.reactive.server.UnwrapException;
3839
import org.jboss.resteasy.reactive.server.core.ExceptionMapping;
3940
import org.jboss.resteasy.reactive.server.model.ContextResolvers;
@@ -134,6 +135,9 @@ public void applicationSpecificUnwrappedExceptions(CombinedIndexBuildItem combin
134135
IndexView index = combinedIndexBuildItem.getIndex();
135136
for (AnnotationInstance instance : index.getAnnotations(UnwrapException.class)) {
136137
AnnotationValue value = instance.value();
138+
AnnotationValue strategyValue = instance.value("strategy");
139+
ExceptionUnwrapStrategy strategy = toExceptionUnwrapStrategy(strategyValue);
140+
137141
if (value == null) {
138142
// in this case we need to use the class where the annotation was placed as the exception to be unwrapped
139143

@@ -162,16 +166,23 @@ public void applicationSpecificUnwrappedExceptions(CombinedIndexBuildItem combin
162166
+ classInfo.name() + "'.");
163167
}
164168

165-
producer.produce(new UnwrappedExceptionBuildItem(classInfo.name().toString()));
169+
producer.produce(new UnwrappedExceptionBuildItem(classInfo.name().toString(), strategy));
166170
} else {
167171
Type[] exceptionTypes = value.asClassArray();
168172
for (Type exceptionType : exceptionTypes) {
169-
producer.produce(new UnwrappedExceptionBuildItem(exceptionType.name().toString()));
173+
producer.produce(new UnwrappedExceptionBuildItem(exceptionType.name().toString(), strategy));
170174
}
171175
}
172176
}
173177
}
174178

179+
private static ExceptionUnwrapStrategy toExceptionUnwrapStrategy(AnnotationValue strategyValue) {
180+
if (strategyValue != null) {
181+
return ExceptionUnwrapStrategy.valueOf(strategyValue.asEnum());
182+
}
183+
return ExceptionUnwrapStrategy.UNWRAP_IF_NO_MATCH;
184+
}
185+
175186
@BuildStep
176187
public ExceptionMappersBuildItem scanForExceptionMappers(CombinedIndexBuildItem combinedIndexBuildItem,
177188
ApplicationResultBuildItem applicationResultBuildItem,
@@ -186,7 +197,7 @@ public ExceptionMappersBuildItem scanForExceptionMappers(CombinedIndexBuildItem
186197
exceptions.addBlockingProblem(BlockingOperationNotAllowedException.class);
187198
exceptions.addBlockingProblem(BlockingNotAllowedException.class);
188199
for (UnwrappedExceptionBuildItem bi : unwrappedExceptions) {
189-
exceptions.addUnwrappedException(bi.getThrowableClassName());
200+
exceptions.addUnwrappedException(bi.getThrowableClassName(), bi.getStrategy());
190201
}
191202
if (capabilities.isPresent(Capability.HIBERNATE_REACTIVE)) {
192203
exceptions.addNonBlockingProblem(

extensions/resteasy-reactive/rest/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/server/spi/UnwrappedExceptionBuildItem.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,35 @@
11
package io.quarkus.resteasy.reactive.server.spi;
22

3+
import org.jboss.resteasy.reactive.server.ExceptionUnwrapStrategy;
4+
35
import io.quarkus.builder.item.MultiBuildItem;
46

57
/**
68
* When an {@link Exception} of this type is thrown and no {@code jakarta.ws.rs.ext.ExceptionMapper} exists,
79
* then RESTEasy Reactive will attempt to locate an {@code ExceptionMapper} for the cause of the Exception.
10+
* <p>
11+
* The unwrapping behavior is controlled by the {@link ExceptionUnwrapStrategy}.
812
*/
913
public final class UnwrappedExceptionBuildItem extends MultiBuildItem {
1014

1115
private final String throwableClassName;
16+
private final ExceptionUnwrapStrategy strategy;
1217

1318
public UnwrappedExceptionBuildItem(String throwableClassName) {
19+
this(throwableClassName, ExceptionUnwrapStrategy.UNWRAP_IF_NO_MATCH);
20+
}
21+
22+
public UnwrappedExceptionBuildItem(String throwableClassName, ExceptionUnwrapStrategy strategy) {
1423
this.throwableClassName = throwableClassName;
24+
this.strategy = strategy;
1525
}
1626

1727
public UnwrappedExceptionBuildItem(Class<? extends Throwable> throwableClassName) {
18-
this.throwableClassName = throwableClassName.getName();
28+
this(throwableClassName.getName(), ExceptionUnwrapStrategy.UNWRAP_IF_NO_MATCH);
29+
}
30+
31+
public UnwrappedExceptionBuildItem(Class<? extends Throwable> throwableClassName, ExceptionUnwrapStrategy strategy) {
32+
this(throwableClassName.getName(), strategy);
1933
}
2034

2135
@Deprecated(forRemoval = true)
@@ -31,4 +45,8 @@ public Class<? extends Throwable> getThrowableClass() {
3145
public String getThrowableClassName() {
3246
return throwableClassName;
3347
}
48+
49+
public ExceptionUnwrapStrategy getStrategy() {
50+
return strategy;
51+
}
3452
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package org.jboss.resteasy.reactive.server;
2+
3+
/**
4+
* Defines the strategy for unwrapping exceptions during exception handling.
5+
*/
6+
public enum ExceptionUnwrapStrategy {
7+
/**
8+
* Always unwraps exception of this type before checking mappers.
9+
* Only falls back to checking mappers for the wrapper exception if no mapper is found for any unwrapped cause.
10+
*/
11+
ALWAYS,
12+
13+
/**
14+
* Unwraps exception only if the thrown exception type itself has no exact mapper registered.
15+
* Checks for an exact match first, then unwraps if none is found.
16+
*/
17+
UNWRAP_IF_NO_EXACT_MATCH,
18+
19+
/**
20+
* Unwraps exceptions only if neither the thrown exception nor any of its supertypes have a registered mapper.
21+
* Checks the entire type hierarchy before unwrapping.
22+
*/
23+
UNWRAP_IF_NO_MATCH
24+
}

independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/UnwrapException.java

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,36 @@
88
/**
99
* Used to configure that an exception (or exceptions) should be unwrapped during exception handling.
1010
* <p>
11-
* Unwrapping means that when an {@link Exception} of the configured type is thrown and no
12-
* {@code jakarta.ws.rs.ext.ExceptionMapper} exists,
13-
* then RESTEasy Reactive will attempt to locate an {@code ExceptionMapper} for the cause of the Exception.
11+
* Unwrapping means that when an {@link Exception} of the configured type is thrown,
12+
* RESTEasy Reactive will attempt to locate an {@code ExceptionMapper} for the cause of the Exception.
13+
* <p>
14+
* The unwrapping behavior is controlled by the {@link #strategy()} attribute:
15+
* <ul>
16+
* <li>{@link ExceptionUnwrapStrategy#ALWAYS}: Always unwraps before checking mappers. Only falls back to
17+
* checking mappers for the wrapper if no mapper is found for any unwrapped cause.</li>
18+
* <li>{@link ExceptionUnwrapStrategy#UNWRAP_IF_NO_EXACT_MATCH}: Unwraps only if the thrown exception type
19+
* itself has no exact mapper. Checks for exact match first, then unwraps if none found.</li>
20+
* <li>{@link ExceptionUnwrapStrategy#UNWRAP_IF_NO_MATCH} (default): Unwraps only if neither the thrown
21+
* exception nor any of its supertypes have a registered mapper.</li>
22+
* </ul>
23+
* <p>
24+
* Example:
25+
*
26+
* <pre>
27+
* &#64;UnwrapException(value = { WebApplicationException.class }, strategy = ExceptionUnwrapStrategy.UNWRAP_IF_NO_EXACT_MATCH)
28+
* public class ExceptionsMappers {
29+
* &#64;ServerExceptionMapper
30+
* public RestResponse&lt;Error&gt; mapUnhandledException(RuntimeException ex) {
31+
* // Handles RuntimeException and its subclasses
32+
* }
33+
*
34+
* &#64;ServerExceptionMapper
35+
* public RestResponse&lt;Error&gt; mapJsonProcessingException(JsonProcessingException ex) {
36+
* // This will be called for JsonProcessingException wrapped in WebApplicationException
37+
* // because UNWRAP_IF_NO_EXACT_MATCH forces unwrapping even though RuntimeException mapper exists
38+
* }
39+
* }
40+
* </pre>
1441
*/
1542
@Retention(RetentionPolicy.RUNTIME)
1643
@Target(ElementType.TYPE)
@@ -20,4 +47,11 @@
2047
* If this is not set, the value is assumed to be the exception class where the annotation is placed
2148
*/
2249
Class<? extends Exception>[] value() default {};
50+
51+
/**
52+
* The unwrapping strategy to use for this exception.
53+
* <p>
54+
* Defaults to {@link ExceptionUnwrapStrategy#UNWRAP_IF_NO_MATCH}.
55+
*/
56+
ExceptionUnwrapStrategy strategy() default ExceptionUnwrapStrategy.UNWRAP_IF_NO_MATCH;
2357
}

independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ExceptionMapping.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@
33
import java.util.ArrayList;
44
import java.util.Collections;
55
import java.util.HashMap;
6-
import java.util.HashSet;
76
import java.util.List;
87
import java.util.Map;
9-
import java.util.Set;
108
import java.util.function.Function;
119
import java.util.function.Predicate;
1210
import java.util.function.Supplier;
1311

1412
import org.jboss.resteasy.reactive.common.model.ResourceExceptionMapper;
13+
import org.jboss.resteasy.reactive.server.ExceptionUnwrapStrategy;
1514
import org.jboss.resteasy.reactive.spi.BeanFactory;
1615

1716
@SuppressWarnings({ "unchecked", "unused" })
@@ -39,7 +38,7 @@ public class ExceptionMapping {
3938
*/
4039
final List<Predicate<Throwable>> blockingProblemPredicates = new ArrayList<>();
4140
final List<Predicate<Throwable>> nonBlockingProblemPredicate = new ArrayList<>();
42-
final Set<String> unwrappedExceptions = new HashSet<>();
41+
final Map<String, ExceptionUnwrapStrategy> unwrappedExceptions = new HashMap<>();
4342

4443
public void addBlockingProblem(Class<? extends Throwable> throwable) {
4544
blockingProblemPredicates.add(new ExceptionTypePredicate(throwable));
@@ -57,11 +56,11 @@ public void addNonBlockingProblem(Predicate<Throwable> predicate) {
5756
nonBlockingProblemPredicate.add(predicate);
5857
}
5958

60-
public void addUnwrappedException(String className) {
61-
unwrappedExceptions.add(className);
59+
public void addUnwrappedException(String className, ExceptionUnwrapStrategy strategy) {
60+
unwrappedExceptions.put(className, strategy);
6261
}
6362

64-
public Set<String> getUnwrappedExceptions() {
63+
public Map<String, ExceptionUnwrapStrategy> getUnwrappedExceptions() {
6564
return unwrappedExceptions;
6665
}
6766

0 commit comments

Comments
 (0)