Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/src/main/asciidoc/rest.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1465,14 +1465,14 @@ There are situations where various Jackson related exceptions need to handled in
This becomes a problem when taking JAX-RS / Jakarta REST rules into account, because the exception mapper `ExceptionMapper` for `MismatchedInputException` would be used instead of the user provide
`ExceptionMapper` for `JsonMappingException` (as `MismatchedInputException` is a subtype of `JsonMappingException`).

One solution for this case is to configure the following:
To handle this, you can disable the built-in exception mapper:

[source,properties]
----
quarkus.class-loading.removed-resources."io.quarkus\:quarkus-rest-jackson"=io/quarkus/resteasy/reactive/jackson/runtime/mappers/BuiltinMismatchedInputExceptionMapper.class
quarkus.rest.exception-mapping.disable-mapper-for=io.quarkus.resteasy.reactive.jackson.runtime.mappers.BuiltinMismatchedInputExceptionMapper
----

which essentially makes Quarkus ignore the `ExceptionMapper` for `MismatchedInputException` completely.
This allows your custom `ExceptionMapper` for `JsonMappingException` to handle all its subclasses, including `MismatchedInputException`.
====

[[secure-serialization]]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package io.quarkus.resteasy.reactive.common.runtime;

import java.util.List;
import java.util.Optional;

import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;
import io.quarkus.runtime.configuration.MemorySize;
Expand All @@ -11,6 +14,11 @@
@ConfigRoot(phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED)
public interface ResteasyReactiveConfig {

/**
* Exception mapping configuration.
*/
ExceptionMappingConfig exceptionMapping();

/**
* The amount of memory that can be used to buffer input before switching to
* blocking IO, up to {@code Long.MAX_VALUE} bytes.
Expand Down Expand Up @@ -81,4 +89,15 @@ public interface ResteasyReactiveConfig {
*/
@WithDefault("true")
boolean removesTrailingSlash();

/**
* Configuration for exception mapping.
*/
interface ExceptionMappingConfig {
/**
* A list of exception mapper classes that should be disabled.
* This allows users to override the default built-in exception mappers provided by Quarkus extensions.
*/
Optional<List<String>> disableMapperFor();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.quarkus.resteasy.reactive.jackson.deployment.test;

import java.util.function.Supplier;

import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import com.fasterxml.jackson.databind.JsonMappingException;

import io.quarkus.test.QuarkusUnitTest;
import io.restassured.RestAssured;

public class ExceptionInReaderWithDisabledBuiltInMapperTest {

@RegisterExtension
static QuarkusUnitTest test = new QuarkusUnitTest()
.setArchiveProducer(new Supplier<>() {
@Override
public JavaArchive get() {
return ShrinkWrap.create(JavaArchive.class)
.addClasses(FroMage.class, FroMageEndpoint.class, CustomJsonMappingExceptionMapper.class);
}
}).overrideConfigKey("quarkus.rest.exception-mapping.disable-mapper-for",
"io.quarkus.resteasy.reactive.jackson.runtime.mappers.BuiltinMismatchedInputExceptionMapper");

@Test
public void test() {
RestAssured.with().contentType("application/json").body("{\"price\": \"ten\"}").put("/fromage")
.then().statusCode(888);
}

@Provider
public static class CustomJsonMappingExceptionMapper implements ExceptionMapper<JsonMappingException> {

@Override
public Response toResponse(JsonMappingException exception) {
return Response.status(888).entity("Custom mapper handled: " + exception.getMessage()).build();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
import io.quarkus.resteasy.reactive.common.deployment.ApplicationResultBuildItem;
import io.quarkus.resteasy.reactive.common.deployment.ResourceInterceptorsContributorBuildItem;
import io.quarkus.resteasy.reactive.common.deployment.ResourceScanningResultBuildItem;
import io.quarkus.resteasy.reactive.common.runtime.ResteasyReactiveConfig;
import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem;
import io.quarkus.resteasy.reactive.server.spi.UnwrappedExceptionBuildItem;
import io.quarkus.resteasy.reactive.spi.ContainerRequestFilterBuildItem;
Expand Down Expand Up @@ -189,11 +190,20 @@ public ExceptionMappersBuildItem scanForExceptionMappers(CombinedIndexBuildItem
BuildProducer<AdditionalBeanBuildItem> additionalBeanBuildItemBuildProducer,
BuildProducer<ReflectiveClassBuildItem> reflectiveClassBuildItemBuildProducer,
List<ExceptionMapperBuildItem> mappers, List<UnwrappedExceptionBuildItem> unwrappedExceptions,
Capabilities capabilities) {
Capabilities capabilities,
ResteasyReactiveConfig config) {
AdditionalBeanBuildItem.Builder beanBuilder = AdditionalBeanBuildItem.builder().setUnremovable();
ExceptionMapping exceptions = ResteasyReactiveExceptionMappingScanner
.scanForExceptionMappers(combinedIndexBuildItem.getComputingIndex(), applicationResultBuildItem.getResult());

if (config.exceptionMapping().disableMapperFor().isPresent()) {
for (String disabledMapper : config.exceptionMapping().disableMapperFor().get()) {
if (disabledMapper != null && !disabledMapper.isEmpty()) {
exceptions.addDisabledMapper(disabledMapper);
}
}
}

exceptions.addBlockingProblem(BlockingOperationNotAllowedException.class);
exceptions.addBlockingProblem(BlockingNotAllowedException.class);
for (UnwrappedExceptionBuildItem bi : unwrappedExceptions) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
Expand All @@ -31,6 +33,11 @@ public class ExceptionMapping {
// this is going to be used when there are mappers that are removable at runtime
final Map<String, List<ResourceExceptionMapper<? extends Throwable>>> runtimeCheckMappers = new HashMap<>();

/**
* Exception mapper class names that should be disabled.
*/
final Set<String> disabledMappers = new HashSet<>();

/**
* Exceptions that indicate an blocking operation was performed on an IO thread.
* <p>
Expand Down Expand Up @@ -126,9 +133,17 @@ public Map<String, List<ResourceExceptionMapper<? extends Throwable>>> getRuntim
return runtimeCheckMappers;
}

public Set<String> getDisabledMappers() {
return disabledMappers;
}

public void addDisabledMapper(String mapperClassName) {
disabledMappers.add(mapperClassName);
}

public Map<String, ResourceExceptionMapper<? extends Throwable>> effectiveMappers() {
if (runtimeCheckMappers.isEmpty()) {
return mappers;
return filterDisabledMappers(mappers);
}
Map<String, ResourceExceptionMapper<? extends Throwable>> result = new HashMap<>();
for (var entry : runtimeCheckMappers.entrySet()) {
Expand All @@ -147,6 +162,20 @@ public Map<String, ResourceExceptionMapper<? extends Throwable>> effectiveMapper
}
}
result.putAll(mappers);
return filterDisabledMappers(result);
}

private Map<String, ResourceExceptionMapper<? extends Throwable>> filterDisabledMappers(
Map<String, ResourceExceptionMapper<? extends Throwable>> mappers) {
if (disabledMappers.isEmpty()) {
return mappers;
}
Map<String, ResourceExceptionMapper<? extends Throwable>> result = new HashMap<>();
for (Map.Entry<String, ResourceExceptionMapper<? extends Throwable>> entry : mappers.entrySet()) {
if (!disabledMappers.contains(entry.getValue().getClassName())) {
result.put(entry.getKey(), entry.getValue());
}
}
return result;
}

Expand Down
Loading