0

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

office.aizaz
  • 179
  • 11

2 Answers2

0

You can manage the appropriate format returned by your ExceptionMapper using the HttpHeaders of JAX-RS API and get the MediaType of the request entity, cf the javadoc : http://docs.oracle.com/javaee/7/api/javax/ws/rs/core/HttpHeaders.html#getMediaType--

So your code would be as follow :

@Provider
public class GenericExceptionMapper implements ExceptionMapper<Exception> {

    @Context
    private HttpHeaders m_headers;

    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).type(m_headers.getMediaType()).build();
    }
}
Rouliboy
  • 1,377
  • 1
  • 8
  • 21
  • Thank for your response. I happen to debug the implementation of RestEasy spec to understand the problem. – office.aizaz Jan 17 '17 at 09:37
  • Your solution only work when ContentType is set in the request headers. This means, I need to explicitly specify the client that ALWAYS specify the content type. Notice if an exception is thrown from your Resource; you don't need to specify mediatype while constructing a response object. The reason is that if no Content Types are specified in the request, the annotation "Produces" is enough to find the MessageBodyWriter for marshalling; however NotAllowedException is special – office.aizaz Jan 17 '17 at 09:45
  • In this case you might get the Acceptable media Type through m_headers.getAcceptableMediaTypes(). Take a look at this thread : http://stackoverflow.com/questions/3227360/jax-rs-jersey-custom-exception-with-xml-or-json – Rouliboy Jan 17 '17 at 10:29
0

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.

office.aizaz
  • 179
  • 11