java-8jersey-2.0

Jersey: Disable standard ExceptionMapper


I developed a RESTfull API with Jersey (2.25.1) and my goal is to always return the same JSON even in case of errors. I then configured a series of exception mappers including one for handling the com.fasterxml.jackson.core.JsonParseException exception.

In the version of Jersey that I am using, there is already the com.fasterxml.jackson.jaxrs.base.JsonParseExceptionMapper mapper that handles the JsonParseException exception. In the JavaDoc comment of this mapper it is explicitly stated: Note that javax.ws.rs.ext.Provider annotation was include up to Jackson 2.7, but removed from 2.8 (as per [jaxrs-providers#22].

However, the mapper is registered (not by me) and in fact in the test class that you will find below, from the printout performed by an ApplicationEventListener, this is evident:

ExceptionMapper: org.glassfish.jersey.server.validation.internal.ValidationExceptionMapper
ExceptionMapper: com.fasterxml.jackson.jaxrs.base.JsonParseExceptionMapper
ExceptionMapper: test.StartupProvidersLoggerTest$JsonParseExceptionMapper
ExceptionMapper: com.fasterxml.jackson.jaxrs.base.JsonMappingExceptionMapper

My problem is that the order of registration of the two mappers is completely random. When mine is registered first, my test succeeds (response media type "application/json"), when the "default" mapper is registered first, my test fails (response media type "text/plain").

As I said, I don't understand why the "default" mapper is recorded, in any case I ask you if there is a way to disable it or give priority to mine (or any other solution).

This is my pom.xml

<properties>
    <java.version>1.8</java.version>
    <jersey.version>2.25.1</jersey.version>
    <junit5.version>5.11.2</junit5.version>
</properties>
<dependencies>
    <!-- Jersey -->
    <dependency>
        <groupId>org.glassfish.jersey.containers</groupId>
        <artifactId>jersey-container-servlet</artifactId>
        <version>${jersey.version}</version>
    </dependency>
    <dependency>
        <groupId>org.glassfish.jersey.media</groupId>
        <artifactId>jersey-media-json-jackson</artifactId>
        <version>${jersey.version}</version>
    </dependency>
    <dependency>
        <groupId>org.glassfish.jersey.core</groupId>
        <artifactId>jersey-client</artifactId>
        <version>${jersey.version}</version>
    </dependency>
    <dependency>
        <groupId>org.glassfish.jersey.ext</groupId>
        <artifactId>jersey-bean-validation</artifactId>
        <version>${jersey.version}</version>
    </dependency>
    <!-- Jersey Test -->
    <dependency>
        <groupId>org.glassfish.jersey.test-framework</groupId>
        <artifactId>jersey-test-framework-core</artifactId>
        <version>${jersey.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.glassfish.jersey.test-framework.providers</groupId>
        <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
        <version>${jersey.version}</version>
        <scope>test</scope>
    </dependency>
    <!-- JUnit -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>${junit5.version}</version>
        <scope>test</scope>
    </dependency>
</dependencies>

This is a simple test class that replicates my problem (but it also happens in production)

package test;

import static org.junit.jupiter.api.Assertions.assertEquals;

import javax.inject.Inject;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.monitoring.ApplicationEvent;
import org.glassfish.jersey.server.monitoring.ApplicationEventListener;
import org.glassfish.jersey.server.monitoring.RequestEvent;
import org.glassfish.jersey.server.monitoring.RequestEventListener;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;

import com.fasterxml.jackson.core.JsonParseException;
import it.upsy.hausmann.hlabahiapi.api.exceptionmapper.AbstractExceptionMapper;
import lombok.Getter;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class JsonParseExceptionMapperTest extends JerseyTest {
    
    @BeforeAll
    public void before() throws Exception {
        super.setUp();
    }

    @AfterAll
    public void after() throws Exception {
        super.tearDown();
    }
    
    @Getter
    static class Consumer {
        private String message;
    }
    
    @Path("test")
    public static class HelloResource {
        @POST
        @Path("hello")
        public Response hello(Consumer consumer) {
            return Response.status(Response.Status.OK)
                    .entity(Entity.entity("{\"result\": true}", MediaType.APPLICATION_JSON))
                    .type(MediaType.APPLICATION_JSON)
                    .build();
        }
    }
    
    @Provider
    public static class StartupProvidersLogger implements ApplicationEventListener {

        @Inject
        private ServiceLocator serviceLocator;

        @Override
        public void onEvent(ApplicationEvent event) {
            if (event.getType() == ApplicationEvent.Type.INITIALIZATION_FINISHED) {
                System.out.println("--- ExceptionMappers at startup ---");
                for (ExceptionMapper<?> mapper : serviceLocator.getAllServices(ExceptionMapper.class)) {
                    System.out.println("ExceptionMapper: " + mapper.getClass().getName());
                }
            }
        }

        @Override
        public RequestEventListener onRequest(RequestEvent requestEvent) {
            return null;
        }
    }
    
    @Provider
    public static class JsonParseExceptionMapper implements ExceptionMapper<JsonParseException> {
        protected static final Logger logger = LogManager.getLogger(AbstractExceptionMapper.class);
        
        @Override
        public Response toResponse(JsonParseException exception) {
            return Response.status(Response.Status.BAD_REQUEST)
                    .entity(Entity.entity("{\"result\": false}", MediaType.APPLICATION_JSON))
                    .type(MediaType.APPLICATION_JSON)
                    .build();
        }
        
    }
    
    @Override
    protected Application configure() {
        ResourceConfig config = new ResourceConfig();
        config.register(HelloResource.class);
        config.register(StartupProvidersLogger.class);
        config.register(JsonParseExceptionMapper.class);
        return config;
    }
    
    @Test
    void doTest() {
        String consumerJson = "wrongJson";
        Response response = target("test").path("hello").request().post(Entity.entity(consumerJson, MediaType.APPLICATION_JSON));
        System.out.println("Response:           " + response);
        System.out.println("Response.MediaType: " + response.getMediaType());
        System.out.println("Response.Payload:   " + response.readEntity(String.class));
        assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus());
        assertEquals(MediaType.APPLICATION_JSON.toString(), response.getMediaType().toString());
    }
}

Solution

  • It seems that adding the @Priority(1) annotation on the custom mapper solves the problem. Also the printing of the mappers registered by ApplicationEventListener now reports my mapper as the absolute first (without the annotation, it was always printed after ValidationExceptionMapper)

    --- ExceptionMappers at startup ---
    ExceptionMapper: test.JsonParseExceptionMapperTest$JsonParseExceptionMapper
    ExceptionMapper: org.glassfish.jersey.server.validation.internal.ValidationExceptionMapper
    ExceptionMapper: com.fasterxml.jackson.jaxrs.base.JsonParseExceptionMapper
    ExceptionMapper: com.fasterxml.jackson.jaxrs.base.JsonMappingExceptionMapper
    

    Updated mapper class

    @Provider
    @Priority(1) // javax.annotation.Priority
    public static class JsonParseExceptionMapper implements ExceptionMapper<JsonParseException> {
        @Override
        public Response toResponse(JsonParseException exception) {
            return Response.status(Response.Status.BAD_REQUEST)
                    .entity(Entity.entity("{\"result\": false}", MediaType.APPLICATION_JSON))
                    .type(MediaType.APPLICATION_JSON)
                    .build();
        }
    }