Search code examples
restresteasyjava-ee-7wildfly-10

RestEasy ExceptionMapper NotAllowedException Serialization Error MediaType octet-stream


I present here the simple rest service to illustrate the exception I receive.

Service A

@Path("/A")
public class ServiceA {

    @GET
    @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
    public Response show() {
        return Response.ok(new User("John", "Doe")).build();
    }
}

Model:

User

@XmlRootElement
public class User {

    private String firstName;
    private String lastName;

    public User() {
    }

    public User(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
}

ErrorResponse

@XmlRootElement
public class ErrorResponse {

    private String errorType;
    private String errorMessage;

    public ErrorResponse() {
    }

    public ErrorResponse(String errorType, String errorMessage) {
        this.errorType = errorType;
        this.errorMessage = errorMessage;
    }

    public String getErrorType() {
        return errorType;
    }

    public void setErrorType(String errorType) {
        this.errorType = errorType;
    }

    public String getErrorMessage() {
        return errorMessage;
    }

    public void setErrorMessage(String errorMessage) {
        this.errorMessage = errorMessage;
    }
}

Finally my ExceptionMapper looks like this:

@Provider
public class GenericExceptionMapper implements ExceptionMapper<Exception> {

    private final Logger LOG = LoggerFactory.getLogger(GenericExceptionMapper.class);

    @Override
    public Response toResponse(Exception exception) {
        ErrorResponse errorResponse = new ErrorResponse(exception.getClass().getSimpleName(), exception.getMessage());

        if (exception instanceof WebApplicationException) {
            LOG.error("Type: {}", exception.getClass().getSimpleName());
            LOG.error("Message: {}", exception.getMessage());
            WebApplicationException webApplicationException = (WebApplicationException) exception;
            return Response.status(webApplicationException.getResponse().getStatus()).entity(errorResponse).build();
        }

        return Response.serverError().entity(errorResponse).build();
    }
}

Calling a GET on the URI I get a correct response:

Accept: application/json
GET http://localhost:8080/exception-mapper-example/rest/A

{"firstName":"John","lastName":"Doe"}

However calling POST on this URI I get an exception:

 2017-01-13 16:53:46,859 ERROR [com.aizaz.samples.exceptionmapper.GenericExceptionMapper] (default task-35) Type: NotAllowedException
 2017-01-13 16:53:46,860 ERROR [com.aizaz.samples.exceptionmapper.GenericExceptionMapper] (default task-35) Message: RESTEASY003650: No resource method found for POST, return 405 with Allow header
 2017-01-13 16:53:46,860 ERROR [io.undertow.request] (default task-35) UT005023: Exception handling request to /exception-mapper-example/rest/A: org.jboss.resteasy.spi.UnhandledException: org.jboss.resteasy.core.NoMessageBodyWriterFoundFailure: Could not find MessageBodyWriter for response object of type: com.aizaz.samples.model.ErrorResponse of media type: application/octet-stream
at org.jboss.resteasy.core.SynchronousDispatcher.writeException(SynchronousDispatcher.java:180)
at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:199)
at org.jboss.resteasy.plugins.server.servlet.ServletContainerDispatcher.service(ServletContainerDispatcher.java:221)
at org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.service(HttpServletDispatcher.java:56)
at org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.service(HttpServletDispatcher.java:51)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
at io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:85)
at io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:62)
at io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36)
at org.wildfly.extension.undertow.security.SecurityContextAssociationHandler.handleRequest(SecurityContextAssociationHandler.java:78)
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:131)
at io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:57)
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:46)
at io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:64)
at io.undertow.security.handlers.AuthenticationMechanismsHandler.handleRequest(AuthenticationMechanismsHandler.java:60)
at io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler.handleRequest(CachedAuthenticatedSessionHandler.java:77)
at io.undertow.security.handlers.NotificationReceiverHandler.handleRequest(NotificationReceiverHandler.java:50)
at io.undertow.security.handlers.AbstractSecurityContextAssociationHandler.handleRequest(AbstractSecurityContextAssociationHandler.java:43)
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at org.wildfly.extension.undertow.security.jacc.JACCContextIdHandler.handleRequest(JACCContextIdHandler.java:61)
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:284)
at io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:263)
at io.undertow.servlet.handlers.ServletInitialHandler.access$000(ServletInitialHandler.java:81)
at io.undertow.servlet.handlers.ServletInitialHandler$1.handleRequest(ServletInitialHandler.java:174)
at io.undertow.server.Connectors.executeRootHandler(Connectors.java:202)
at io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:793)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
Caused by: org.jboss.resteasy.core.NoMessageBodyWriterFoundFailure: Could not find MessageBodyWriter for response object of type: com.aizaz.samples.model.ErrorResponse of media type: application/octet-stream
at org.jboss.resteasy.core.ServerResponseWriter.writeNomapResponse(ServerResponseWriter.java:66)
at org.jboss.resteasy.core.SynchronousDispatcher.writeException(SynchronousDispatcher.java:176)
... 32 more

Relevant part

Caused by: org.jboss.resteasy.core.NoMessageBodyWriterFoundFailure:
Could not find MessageBodyWriter for response object of type:
com.aizaz.samples.model.ErrorResponse of media type: application/octet-stream

Obviously serialization failed since the media type: application/octet-stream.

I know I can explicitly specify media type when building a response such as

 Response.ok().type(MediaType.APPLICATION_JSON).build();

But I don't want to do that; since I accept both JSON/XML Accept headers and would like to sent back an appropriate response in JSON or XML format.

  1. How can I accomplish this?
  2. Why in this case Response is created with media type octet-stream?

I mean if I create my custom exception which is mapped by the same ExceptionMapper as described in the code; the Response object doesn't need to explicitly specify MediaType.

Would be really nice if someone can provide me his/her valuable wisdom


Solution

  • In case of custom exceptions which extend WebApplicationException such as

    public class MyCustomException extends WebApplicationException 
    

    does not require an explicit ExceptionMapper<MyCustomerException> for exception handling and response creation.

    ExceptionMapper can be very helpful to handle exceptions which derive from Exception (or its subclasses) but not WebApplicationException (and its subclasses) (note WebApplicationException is also a child class of Exception)

    For example ExceptionMapper can be used to handle an exception such as IllegalArgumentException and creating a response.

    In both case above the response can be serialized according to the @Producesspecified on the Resource method.

    However after looking at the spec implementation of RestEasy I found out, the even for WebApplicationException(s), if an ExceptionMapper is @Provided by the rest service, it will be trigged.

    resteasy-jaxrs:3.1.0.Final
    class: ExceptionHandler
    method: public Response handleException(HttpRequest request, Throwable e)
    
      // First try and handle it with a mapper
      if ((jaxrsResponse = executeExceptionMapper(e)) != null) {
         return jaxrsResponse;
      }
    

    So either I make sure that ExceptionMapper is used for some specific Exceptions such as ExceptionMapper<IllegalArgumentException> instead of catching all exceptions as shown above in the code ExceptionMapper<Exception> or simply return the response as shown in the code below:

     if (exception instanceof WebApplicationException) {
        WebApplicationException webApplicationException = (WebApplicationException) exception;
        return webApplicationException.getResponse();
    }
    

    The serialization error will not occur. Why? because the framework takes care of this (based on @Produces annotation it will serialize the response for NON WebApplicationException based responses. And for WebApplicationException based response as shown above, framework will take care of the response as well (since ErrorResponse entity was never used)

    However coming to the problem mentioned in this ticket. NotAllowedException occurs in the spec implementation code before the method associated with URI gets executed. Thus the @Produces annotation doesn't take effect and while Marshalling the response, a default MediaType octet-stream is used.

    resteasy-jaxrs:3.1.0.Final
    class: SegmentNode
    method: public Match match(List<Match> matches, String httpMethod, HttpRequest request)
    

    So while the following exceptions occur; DefaultOptionsMethodException NotAllowedException NotSupportedException NotAcceptableException

    request attribute RESTEASY_CHOSEN_ACCEPT doesn't get set

      request.setAttribute(RESTEASY_CHOSEN_ACCEPT, sortEntry.getAcceptType());
      return sortEntry.match;
    

    and when the server tries to write a response it doesn't find the MediaType (as we never set it while creating a response object)

    resteasy-jaxrs:3.1.0.Final
    class: ServerResponseWriter
    method: public static void writeNomapResponse(BuiltResponse jaxrsResponse, final HttpRequest request, ...
    
     if (jaxrsResponse.getEntity() != null && jaxrsResponse.getMediaType() == null) {
         setDefaultContentType(request, jaxrsResponse, providerFactory, method);
     }
    

    It tries to set it from method annotations; which as mentioned before were never set, since the NotAllowedException occurred before RESTEASY_CHOSEN_ACCEPT could have been set.

    It finds a wild card as no accept headers were specified and thus octet stream was set

    resteasy-jaxrs:3.1.0.Final
    class: ServerResponseWriter
    method: protected static void setDefaultContentType(HttpRequest request, BuiltResponse ...
    if (chosen.isWildcardType()) {
         chosen = MediaType.APPLICATION_OCTET_STREAM_TYPE;
    }
    

    (this a just a summary; for detailed steps I must go back to the spec implementation code)

    Thus I must specify a mediatype. This can done looking at the HttpHeaders from @Context to make it dynamic or if nothing is specified in the headers as in my case, I provide a default MediaType Application/XML for serialization to proceed.

    Hope this helps someone also facing the same issue.