Jersey custom ExceptionMapper not used if exception occurs when processing request body - java

I'm facing a tricky behavior from my REST resource.
The exposed method is expecting a complex json object :
#Path(RestURIConstant.NOTIFICATION_ROOT_URI)
#Component
#Scope("request")
public class NotificationResource implements RestURIConstant {
/** Notification service. */
#Autowired
private INotificationService notificationService;
#Path(RestURIConstant.COMPLEMENT_URI)
#POST
#Consumes(MediaType.APPLICATION_JSON)
#Produces(MediaType.APPLICATION_JSON)
public Response processNotification(final EventDTO event)
throws BusinessException, TechnicalException {
checkParameters(event);
notificationService.processEvent(event);
return Response.ok().build();
}
}
EventDTO has two enum fields : notificationType and eventType :
public class EventDTO {
private ENotificationType notificationType;
private EEventType eventType;
private String eventDate;
private String userName;
//... getters, setters
}
What I want is to map exception from any kind of data validation error to get at the end a json response with an error code and error message. And after following jax-rs jersey: Exception Mapping for Enum bound FormParam :
So for the ENoticationType I wrote :
public enum ENotificationEventType {
RULE,
ALARM,
ACK,
INFO;
#JsonCreator
public static ENotificationEventType fromString(final String typeCode)
throws BusinessException {
if (typeCode == null) {
throw new BusinessException(ValidationCode.VALUE_REQUIRED, "type");
}
try {
return valueOf(typeCode);
} catch (final IllegalArgumentException iae) {
throw new BusinessException(ValidationCode.UNSUPPORTED_VALUE, "type", typeCode, Arrays.toString(values()));
}
}
}
And for the Mapper I wrote :
#Provider
#Singleton
public class BusinessExceptionMapper implements ExceptionMapper<BusinessException> {
#Override
public Response toResponse(final BusinessException exception) {
Status status = Status.INTERNAL_SERVER_ERROR;
// If a validationCode error = unsupported version => CODE 410
if (exception.getErrorCode().equals(ValidationCode.UNSUPPORTED_API_VERSION)) {
status = Status.GONE;
} else if (exception.getErrorCode().getClass().isAssignableFrom(ValidationCode.class)) {
// If a validationCode error then BAD_REQUEST (400) HTTP
status = Status.BAD_REQUEST;
} else if (exception.getErrorCode().getClass().isAssignableFrom(NotFoundCode.class)) { // CODE 404
status = Status.NOT_FOUND;
} else if (exception.getErrorCode().getClass().isAssignableFrom(SecurityCode.class)) { // CODE 401
status = Status.UNAUTHORIZED;
} else if (exception.getErrorCode().getClass().isAssignableFrom(AdminSecurityCode.class)) { // CODE 401
status = Status.UNAUTHORIZED;
}
return Response.status(status).type(MediaType.APPLICATION_JSON)
.entity(ErrorMessageHelper.createErrorMessageHelper(
exception.getErrorCode(), exception.getMessage()))
.build();
}
And my application-context contains <context:component-scan base-package=" com.technicolor.hovis.backend.rest, com.technicolor.hovis.admin.rest" />
I already read several answers to questions relative to Exception mapping in jersey but in my case, it's not that the mapping is not recognized but that it's not applied in all cases :
the exceptions thrown by checkParameters are mapped and the result is as expected
but if an invalid enum is sent, the #JsonCreator method is called, throw the same type of exception but this one is not mapped as expected.
So The response looks like :
<data contentType="text/plain;charset=UTF-8" contentLength="176">
<![CDATA[Unsupported type value : 'ALARN'. Expected values are [RULE, ALARM, ACK, INFO] (through reference chain: EventDTO["type"])]]>
</data>
And not the expected :
{
"code": 6,
"message": "Unsupported type value : 'ALARN'. Expected values are [RULE, ALARM, ACK, INFO]"
}
Any idea ?
Cyril

I would try changing the BusinessExceptionMapper to:
public class BusinessExceptionMapper implements ExceptionMapper<Exception>
If I understand correctly, the problem is when deserializng the parameter you except, in that case an IOException will be thrown and not BusinessException which I am guessing is your custom notification. So you can either extend ExceptionMapper<Exception> or ExceptionMapper<IOException>

I noticed that when I throw a checked exception in JSONCreator (just like you did), Jackson catches it and wraps it in IllegalArgumentException, so IllegalArgumentException ends up in ExceptionMapper.
Maybe this was cause of your problems?

Related

How to keep exception Message in modelmapper converter

I am using ModelMapper and there I define some PostConverters like:
modelMapper
.createTypeMap(TestDTO.class, Test.class)
.setPostConverter(converter -> {
Test dest = converter.getDestination();
TestDTO source = converter.getSource();
if (source.getDependency() != null) {
Dependency dependency = dependencyRepository
.findById(source.getDependency().getId())
.orElseThrow(() -> new BadRequestException("Invalid Dependency"));
dest.setDependency(dependency);
}
return dest;
});
The problem with that code is that, if I put an invalid id for the dependency, then I receive a response payload like:
{
"timestamp": "2018-09-18T13:51:05.203+0200",
"status": 400,
"error": "Bad Request",
"message": "ModelMapper mapping errors:\n\n1) Converter ....",
"path": "/api/test"
}
But I want that in the error message I get Invalid Dependency. Any Idea how to do it?
Here is the Definition of the BadRequestException:
#ResponseStatus(HttpStatus.BAD_REQUEST)
public class BadRequestException extends RunTimeException {
public BadRequestException() {
super("Bad Request");
}
public BadRequestException(String message) {
super(message);
}
public BadRequestException(String pattern, Object... parameters) {
super(pattern, parameters);
}
}
The fault seems to be in your exception handling with the annotation, see here:
https://docs.spring.io/spring/docs/4.2.4.RELEASE/javadoc-api/org/springframework/web/bind/annotation/ResponseStatus.html
It says:
"Warning: when using this annotation on an exception class, or when setting the reason attribute of this annotation, the HttpServletResponse.sendError method will be used."
It seems like the method does not use your exception message by default.
So you could either try defining a 'hardcoded' message and give a concrete string with
#ResponseStatus(value = HttpStatus.BadRequest, reason = "Invalid Dependency")
Or do it in a kind of this way:
public class BadRequestException extends RunTimeException {
public BadRequestException() {
super("Bad Request");
}
public BadRequestException(String message) {
super(message);
}
public BadRequestException(String pattern, Object... parameters) {
super(pattern, parameters);
}
}
#ControllerAdvice
public class CustomExceptionResolver {
#ExceptionHandler(BadRequestException.class)
public ResponseEntity<Error> resolveAndSendException(BadRequestException e) throws IOException {
return new ResponseEntity<Error>(e, HttpStatus.BadRequest);
}
}
I would prefer the first one, with a slight change:
Introduce a new class called InvalidDependencyException and apply the first example to this class, so you have your message and still can use the integrated messages for any other bad request. (Throw it of course in your stream-part)

Netflix Feign - Propagate Status and Exception through Microservices

I'm using Netflix Feign to call to one operation of a Microservice A to other other operation of a Microservice B which validates a code using Spring Boot.
The operation of Microservice B throws an exception in case of the validation has been bad. Then I handled in the Microservices and return a HttpStatus.UNPROCESSABLE_ENTITY (422) like next:
#ExceptionHandler({
ValidateException.class
})
#ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
#ResponseBody
public Object validationException(final HttpServletRequest request, final validateException exception) {
log.error(exception.getMessage(), exception);
error.setErrorMessage(exception.getMessage());
error.setErrorCode(exception.getCode().toString());
return error;
}
So, when Microservice A calls to B in a interface as next:
#Headers("Content-Type: " + MediaType.APPLICATION_JSON_UTF8_VALUE)
#RequestLine("GET /other")
void otherOperation(#Param("other") String other );
#Headers("Content-Type: " + MediaType.APPLICATION_JSON_UTF8_VALUE)
#RequestLine("GET /code/validate")
Boolean validate(#Param("prefix") String prefix);
static PromotionClient connect() {
return Feign.builder()
.encoder(new GsonEncoder())
.decoder(new GsonDecoder())
.target(PromotionClient.class, Urls.SERVICE_URL.toString());
}
and the validations fails it returns a internal error 500 with next message:
{
"timestamp": "2016-08-05T09:17:49.939+0000",
"status": 500,
"error": "Internal Server Error",
"exception": "feign.FeignException",
"message": "status 422 reading Client#validate(String); content:\n{\r\n \"errorCode\" : \"VALIDATION_EXISTS\",\r\n \"errorMessage\" : \"Code already exists.\"\r\n}",
"path": "/code/validate"
}
But I need to return the same as the Microservice operation B.
Which would be the best ways or techniques to propagate Status and Exceptions through microservices using Netflix Feign?
You could use a feign ErrorDecoder
https://github.com/OpenFeign/feign/wiki/Custom-error-handling
Here is an example
public class MyErrorDecoder implements ErrorDecoder {
private final ErrorDecoder defaultErrorDecoder = new Default();
#Override
public Exception decode(String methodKey, Response response) {
if (response.status() >= 400 && response.status() <= 499) {
return new MyBadRequestException();
}
return defaultErrorDecoder.decode(methodKey, response);
}
}
For spring to pick up the ErrorDecoder you have to put it on the ApplicationContext:
#Bean
public MyErrorDecoder myErrorDecoder() {
return new MyErrorDecoder();
}
Shameless plug for a little library I did that uses reflection to dynamically rethrow checked exceptions (and unchecked if they are on the Feign interface) based on an error code returned in the body of the response.
More information on the readme :
https://github.com/coveo/feign-error-decoder
OpenFeign's FeignException doesn't bind to a specific HTTP status (i.e. doesn't use Spring's #ResponseStatus annotation), which makes Spring default to 500 whenever faced with a FeignException. That's okay because a FeignException can have numerous causes that can't be related to a particular HTTP status.
However you can change the way that Spring handles FeignExceptions. Simply define an ExceptionHandler that handles the FeignException the way you need it (see here):
#RestControllerAdvice
public class GlobalExceptionHandler {
#ExceptionHandler(FeignException.class)
public String handleFeignStatusException(FeignException e, HttpServletResponse response) {
response.setStatus(e.status());
return "feignError";
}
}
This example makes Spring return the same HTTP status that you received from Microservice B. You can go further and also return the original response body:
response.getOutputStream().write(e.content());
Write your custom exception mapper and register it. You can customize responses.
Complete example is here
public class GenericExceptionMapper implements ExceptionMapper<Throwable> {
#Override
public Response toResponse(Throwable ex) {
return Response.status(500).entity(YOUR_RETURN_OBJ_HERE).build();
}
}
Since 2017 we've created a library that does this from annotations (making it fairly easy to, just like for requests/etc, to code this up by annotations).
it basically allows you to code error handling as follows:
#ErrorHandling(codeSpecific =
{
#ErrorCodes( codes = {401}, generate = UnAuthorizedException.class),
#ErrorCodes( codes = {403}, generate = ForbiddenException.class),
#ErrorCodes( codes = {404}, generate = UnknownItemException.class),
},
defaultException = ClassLevelDefaultException.class
)
interface GitHub {
#ErrorHandling(codeSpecific =
{
#ErrorCodes( codes = {404}, generate = NonExistentRepoException.class),
#ErrorCodes( codes = {502, 503, 504}, generate = RetryAfterCertainTimeException.class),
},
defaultException = FailedToGetContributorsException.class
)
#RequestLine("GET /repos/{owner}/{repo}/contributors")
List<Contributor> contributors(#Param("owner") String owner, #Param("repo") String repo);
}
You can find it in the OpenFeign organisation:
https://github.com/OpenFeign/feign-annotation-error-decoder
disclaimer: I'm a contributor to feign and the main dev for that error decoder.
What we do is as follows:
Share common jar which contains exceptions with both microservices.
1.) In microservices A convert exception to a DTO class lets say ErrorInfo.
Which will contain all the attributes of your custom exception with a String exceptionType, which will contain exception class name.
2.) When it is received at microservice B it will be handled by ErrorDecoder in microservice B and It will try to create an exception object from exceptionType as below:
#Override
public Exception decode(String methodKey, Response response) {
ErrorInfo errorInfo = objectMapper.readValue(details, ErrorInfo.class);
Class exceptionClass;
Exception decodedException;
try {
exceptionClass = Class.forName(errorInfo.getExceptionType());
decodedException = (Exception) exceptionClass.newInstance();
return decodedException;
}
catch (ClassNotFoundException e) {
return new PlatformExecutionException(details, errorInfo);
}
return defaultErrorDecoder.decode(methodKey, response);
}

Customizing Zuul Exception

I have a scenario in Zuul where the service that the URL is routed too might be down . So the reponse body gets thrown with 500 HTTP Status and ZuulException in the JSON body response.
{
"timestamp": 1459973637928,
"status": 500,
"error": "Internal Server Error",
"exception": "com.netflix.zuul.exception.ZuulException",
"message": "Forwarding error"
}
All I want to do is to customise or remove the JSON response and maybe change the HTTP status Code.
I tried to create a exception Handler with #ControllerAdvice but the exception is not grabbed by the handler.
UPDATES:
So I extended the Zuul Filter I can see it getting into the run method after the error has been executed how do i change the response then. Below is what i got so far. I read somewhere about SendErrorFilter but how do i implement that and what does it do?
public class CustomFilter extends ZuulFilter {
#Override
public String filterType() {
return "post";
}
#Override
public int filterOrder() {
return 1;
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public Object run() {
final RequestContext ctx = RequestContext.getCurrentContext();
final HttpServletResponse response = ctx.getResponse();
if (HttpStatus.INTERNAL_SERVER_ERROR.value() == ctx.getResponse().getStatus()) {
try {
response.sendError(404, "Error Error"); //trying to change the response will need to throw a JSON body.
} catch (final IOException e) {
e.printStackTrace();
} ;
}
return null;
}
Added this to the class that has #EnableZuulProxy
#Bean
public CustomFilter customFilter() {
return new CustomFilter();
}
We finally got this working [Coded by one of my colleague]:-
public class CustomErrorFilter extends ZuulFilter {
private static final Logger LOG = LoggerFactory.getLogger(CustomErrorFilter.class);
#Override
public String filterType() {
return "post";
}
#Override
public int filterOrder() {
return -1; // Needs to run before SendErrorFilter which has filterOrder == 0
}
#Override
public boolean shouldFilter() {
// only forward to errorPath if it hasn't been forwarded to already
return RequestContext.getCurrentContext().containsKey("error.status_code");
}
#Override
public Object run() {
try {
RequestContext ctx = RequestContext.getCurrentContext();
Object e = ctx.get("error.exception");
if (e != null && e instanceof ZuulException) {
ZuulException zuulException = (ZuulException)e;
LOG.error("Zuul failure detected: " + zuulException.getMessage(), zuulException);
// Remove error code to prevent further error handling in follow up filters
ctx.remove("error.status_code");
// Populate context with new response values
ctx.setResponseBody(“Overriding Zuul Exception Body”);
ctx.getResponse().setContentType("application/json");
ctx.setResponseStatusCode(500); //Can set any error code as excepted
}
}
catch (Exception ex) {
LOG.error("Exception filtering in custom error filter", ex);
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
}
The Zuul RequestContext doesn't contain the error.exception as mentioned in this answer.
Up to date the Zuul error filter:
#Component
public class ErrorFilter extends ZuulFilter {
private static final Logger LOG = LoggerFactory.getLogger(ErrorFilter.class);
private static final String FILTER_TYPE = "error";
private static final String THROWABLE_KEY = "throwable";
private static final int FILTER_ORDER = -1;
#Override
public String filterType() {
return FILTER_TYPE;
}
#Override
public int filterOrder() {
return FILTER_ORDER;
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public Object run() {
final RequestContext context = RequestContext.getCurrentContext();
final Object throwable = context.get(THROWABLE_KEY);
if (throwable instanceof ZuulException) {
final ZuulException zuulException = (ZuulException) throwable;
LOG.error("Zuul failure detected: " + zuulException.getMessage());
// remove error code to prevent further error handling in follow up filters
context.remove(THROWABLE_KEY);
// populate context with new response values
context.setResponseBody("Overriding Zuul Exception Body");
context.getResponse().setContentType("application/json");
// can set any error code as excepted
context.setResponseStatusCode(503);
}
return null;
}
}
I had the same problem and was able to solve it in simpler way
Just put this into you Filter run() method
if (<your condition>) {
ZuulException zuulException = new ZuulException("User message", statusCode, "Error Details message");
throw new ZuulRuntimeException(zuulException);
}
and SendErrorFilter will deliver to the user the message with the desired statusCode.
This Exception in an Exception pattern does not look exactly nice, but it works here.
Forwarding is often done by a filter, in this case the request does not even reach a controller. This would explain why your #ControllerAdvice does not work.
If you forward in the controller than the #ControllerAdvice should work.
Check if spring creates an instance of the class annotated with #ControllerAdvice. For that place a breakpoint in the class and see whether it is hit.
Add a breakpoint also in the controller method where the forwarding should happen. May be you accidently invoke another controller method than you inspect ?
These steps should help you resolve the issue.
In your class annotated with #ControllerAdvice add an ExceptionHandler method annotated with #ExceptionHandler(Exception.class), that should catch every Exception.
EDIT :
You can try to add your own filter that converts the error response returned by the Zuulfilter. There you can change the response as you like.
How the error response can be customized is explained here :
exception handling for filter in spring
Placing the filter correctly may be a little tricky.
Not exactly sure about the correct position, but you should be aware of the order of your filters and the place where you handle the exception.
If you place it before the Zuulfilter, you have to code your error handling after calling doFilter().
If you place it after the Zuulfilter, you have to code your error handling before calling doFilter().
Add breakpoints in your filter before and after doFilter() may help to find the correct position.
Here are the steps to do it with #ControllerAdvice:
First add a filter of type error and let it be run before the SendErrorFilter in zuul itself.
Make sure to remove the key associated with the exception from the RequestContext to prevent the SendErrorFilter from executing.
Use RequestDispatcher to forward the request to the ErrorController -- explained below.
Add a #RestController class and make it extends AbstractErrorController, and re-throw the exception again (add it in the step of executing your new error filter with (key, exception), get it from the RequestContext in your controller).
The exception will now be caught in your #ControllerAdvice class.
The simplest solution is to follow first 4 steps.
1. Create your own CustomErrorController extends
AbstractErrorController which will not allow the
BasicErrorController to be called.
2. Customize according to your need refer below method from
BasicErrorController.
<pre><code>
#RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
return new ResponseEntity<>(body, status);
}
</pre></code>
4. You can control whether you want exception / stack trace to be printed or not can do as mentioned below:
<pre><code>
server.error.includeException=false
server.error.includeStacktrace=ON_TRACE_PARAM
</pre></code>
====================================================
5. If you want all together different error response re-throw your custom exception from your CustomErrorController and implement the Advice class as mentioned below:
<pre><code>
#Controller
#Slf4j
public class CustomErrorController extends BasicErrorController {
public CustomErrorController(ErrorAttributes errorAttributes, ServerProperties serverProperties,
List<ErrorViewResolver> errorViewResolvers) {
super(errorAttributes, serverProperties.getError(), errorViewResolvers);
log.info("Created");
}
#Override
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
throw new CustomErrorException(String.valueOf(status.value()), status.getReasonPhrase(), body);
}
}
#ControllerAdvice
public class GenericExceptionHandler {
// Exception handler annotation invokes a method when a specific exception
// occurs. Here we have invoked Exception.class since we
// don't have a specific exception scenario.
#ExceptionHandler(CustomException.class)
#ResponseBody
public ErrorListWsDTO customExceptionHandle(
final HttpServletRequest request,
final HttpServletResponse response,
final CustomException exception) {
LOG.info("Exception Handler invoked");
ErrorListWsDTO errorData = null;
errorData = prepareResponse(response, exception);
response.setStatus(Integer.parseInt(exception.getCode()));
return errorData;
}
/**
* Prepare error response for BAD Request
*
* #param response
* #param exception
* #return
*/
private ErrorListWsDTO prepareResponse(final HttpServletResponse response,
final AbstractException exception) {
final ErrorListWsDTO errorListData = new ErrorListWsDTO();
final List<ErrorWsDTO> errorList = new ArrayList<>();
response.setStatus(HttpStatus.BAD_REQUEST.value());
final ErrorWsDTO errorData = prepareErrorData("500",
"FAILURE", exception.getCause().getMessage());
errorList.add(errorData);
errorListData.setErrors(errorList);
return errorListData;
}
/**
* This method is used to prepare error data
*
* #param code
* error code
* #param status
* status can be success or failure
* #param exceptionMsg
* message description
* #return ErrorDTO
*/
private ErrorWsDTO prepareErrorData(final String code, final String status,
final String exceptionMsg) {
final ErrorWsDTO errorDTO = new ErrorWsDTO();
errorDTO.setReason(code);
errorDTO.setType(status);
errorDTO.setMessage(exceptionMsg);
return errorDTO;
}
}
</pre></code>
This is what worked for me. RestExceptionResponse is the class which is used within the #ControllerAdvice, so we have an identical exception response in case of internal ZuulExceptions.
#Component
#Log4j
public class CustomZuulErrorFilter extends ZuulFilter {
private static final String SEND_ERROR_FILTER_RAN = "sendErrorFilter.ran";
#Override
public String filterType() {
return ERROR_TYPE;
}
#Override
public int filterOrder() {
return SEND_ERROR_FILTER_ORDER - 1; // Needs to run before SendErrorFilter which has filterOrder == 0
}
#Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
Throwable ex = ctx.getThrowable();
return ex instanceof ZuulException && !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false);
}
#Override
public Object run() {
try {
RequestContext ctx = RequestContext.getCurrentContext();
ZuulException ex = (ZuulException) ctx.getThrowable();
// log this as error
log.error(StackTracer.toString(ex));
String requestUri = ctx.containsKey(REQUEST_URI_KEY) ? ctx.get(REQUEST_URI_KEY).toString() : "/";
RestExceptionResponse exceptionResponse = new RestExceptionResponse(HttpStatus.INTERNAL_SERVER_ERROR, ex, requestUri);
// Populate context with new response values
ctx.setResponseStatusCode(500);
this.writeResponseBody(ctx.getResponse(), exceptionResponse);
ctx.set(SEND_ERROR_FILTER_RAN, true);
}
catch (Exception ex) {
log.error(StackTracer.toString(ex));
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
private void writeResponseBody(HttpServletResponse response, Object body) throws IOException {
response.setContentType("application/json");
try (PrintWriter writer = response.getWriter()) {
writer.println(new JSonSerializer().toJson(body));
}
}
}
The output looks like this:
{
"timestamp": "2020-08-10 16:18:16.820",
"status": 500,
"error": "Internal Server Error",
"path": "/service",
"exception": {
"message": "Filter threw Exception",
"exceptionClass": "com.netflix.zuul.exception.ZuulException",
"superClasses": [
"com.netflix.zuul.exception.ZuulException",
"java.lang.Exception",
"java.lang.Throwable",
"java.lang.Object"
],
"stackTrace": null,
"cause": {
"message": "com.netflix.zuul.exception.ZuulException: Forwarding error",
"exceptionClass": "org.springframework.cloud.netflix.zuul.util.ZuulRuntimeException",
"superClasses": [
"org.springframework.cloud.netflix.zuul.util.ZuulRuntimeException",
"java.lang.RuntimeException",
"java.lang.Exception",
"java.lang.Throwable",
"java.lang.Object"
],
"stackTrace": null,
"cause": {
"message": "Forwarding error",
"exceptionClass": "com.netflix.zuul.exception.ZuulException",
"superClasses": [
"com.netflix.zuul.exception.ZuulException",
"java.lang.Exception",
"java.lang.Throwable",
"java.lang.Object"
],
"stackTrace": null,
"cause": {
"message": "Load balancer does not have available server for client: template-scalable-service",
"exceptionClass": "com.netflix.client.ClientException",
"superClasses": [
"com.netflix.client.ClientException",
"java.lang.Exception",
"java.lang.Throwable",
"java.lang.Object"
],
"stackTrace": null,
"cause": null
}
}
}
}
}

ExceptionMapper not working as expected

I am using jersey for REST web services.
I am handling all 404 responses by throwing NotFoundException(Package com.sun.jersey.api) whenever I don't get any object from service layer.
e.g.
#GET
#Path("/{folderID}")
#Produces(MediaType.APPLICATION_JSON)
public Response getFolder(#PathParam("folderID") int folderID) {
.
.
.
Folder folderObj = folderService.getFolder(folderID);
if(folderObj == null){
throw new NotFoundException("Folder with ID '"+folderID+"' not found.");
}
}
I have written ExceptionMapper for this exception.
#Provider
public class NotFoundExceptionMapper implements ExceptionMapper<NotFoundException> {
public Response toResponse(NotFoundException ex) {
ErrorMesage errorMessage = new ErrorMessage();
errorMessage.setCode(Status.NOT_FOUND.getStatusCode());
errorMessage.setMessage(ex.getMessage());
return Response.status(Status.NOT_FOUND)
.entity(errorMessage)
.type(MediaType.APPLICATION_JSON)
.build();
}
}
So When I give unknown folder ID as path parameter, exception is thrown but code in NotFoundExceptionMapper is not invoked. (I can see exception message in response but as 'plain text', even though in mapper I am returning response in JSON; and debug break point is also not hit).
Also, Above exception mapper is invoked when I enter incorrect resource name in URI, but not for incorrect path param.
I have also added exception mapper like below to respond to all other exceptions.
public class GenericExceptionMapper implements ExceptionMapper<Throwable>{
public Response toResponse(Throwable ex) {
ErrorMessage errorMessage = new ErrorMessage();
errorMessage.setCode(Status.INTERNAL_SERVER_ERROR.getStatusCode());
errorMessage.setMessage(ex.getMessage());
return Response.status(errorMessage.getCode())
.entity(errorMessage)
.type(MediaType.APPLICATION_JSON)
.build();
}
Above code is called whenever any exception (other than mapped exceptions) is thrown and I get proper JSON response.
So what is wrong with NotFoundException here?
I have googled about this and also looked into source of NotFoundException but didn't find anything useful, please guide me on this.
A snippet from the jersey ServerRuntime class. It has a special logic that if the Exception is an instance of WebApplicationException and has a body, it does not go to the exception mappers at all.
private Response mapException(Throwable originalThrowable) throws Throwable {
if (throwable instanceof WebApplicationException) {
WebApplicationException webApplicationException = (WebApplicationException)throwable;
}
this.processingContext.routingContext().setMappedThrowable(throwable);
waeResponse = webApplicationException.getResponse();
if (waeResponse.hasEntity()) {
LOGGER.log(java.util.logging.Level.FINE, LocalizationMessages.EXCEPTION_MAPPING_WAE_ENTITY(waeResponse.getStatus()), throwable);
return waeResponse;
}
long timestamp = this.tracingLogger.timestamp(ServerTraceEvent.EXCEPTION_MAPPING);
ExceptionMapper mapper = this.runtime.exceptionMappers.findMapping(throwable);
}

Handling custom error response in JAX-RS 2.0 client library

I am starting to use the new client API library in JAX-RS and really loving it so far. I have found one thing I cannot figure out however. The API I am using has a custom error message format that looks like this for example:
{
"code": 400,
"message": "This is a message which describes why there was a code 400."
}
It returns 400 as the status code but also includes a descriptive error message to tell you what you did wrong.
However the JAX-RS 2.0 client is re-mapping the 400 status into something generic and I lose the good error message. It correctly maps it to a BadRequestException, but with a generic "HTTP 400 Bad Request" message.
javax.ws.rs.BadRequestException: HTTP 400 Bad Request
at org.glassfish.jersey.client.JerseyInvocation.convertToException(JerseyInvocation.java:908)
at org.glassfish.jersey.client.JerseyInvocation.translate(JerseyInvocation.java:770)
at org.glassfish.jersey.client.JerseyInvocation.access$500(JerseyInvocation.java:90)
at org.glassfish.jersey.client.JerseyInvocation$2.call(JerseyInvocation.java:671)
at org.glassfish.jersey.internal.Errors.process(Errors.java:315)
at org.glassfish.jersey.internal.Errors.process(Errors.java:297)
at org.glassfish.jersey.internal.Errors.process(Errors.java:228)
at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:424)
at org.glassfish.jersey.client.JerseyInvocation.invoke(JerseyInvocation.java:667)
at org.glassfish.jersey.client.JerseyInvocation$Builder.method(JerseyInvocation.java:396)
at org.glassfish.jersey.client.JerseyInvocation$Builder.get(JerseyInvocation.java:296)
Is there some sort of interceptor or custom error handler that can be injected so that I get access to the real error message. I've been looking through documentation but can't see any way of doing it.
I am using Jersey right now, but I tried this using CXF and got the same result. Here is what the code looks like.
Client client = ClientBuilder.newClient().register(JacksonFeature.class).register(GzipInterceptor.class);
WebTarget target = client.target("https://somesite.com").path("/api/test");
Invocation.Builder builder = target.request()
.header("some_header", value)
.accept(MediaType.APPLICATION_JSON_TYPE)
.acceptEncoding("gzip");
MyEntity entity = builder.get(MyEntity.class);
UPDATE:
I implemented the solution listed in the comment below. It is slightly different since the classes have changed a bit in the JAX-RS 2.0 client API. I still think it is wrong that the default behavior is to give a generic error message and discard the real one. I understand why it wouldn't parse my error object, but the un-parsed version should have been returned. I end up having the replicate exception mapping that the library already does.
Thanks for the help.
Here is my filter class:
#Provider
public class ErrorResponseFilter implements ClientResponseFilter {
private static ObjectMapper _MAPPER = new ObjectMapper();
#Override
public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException {
// for non-200 response, deal with the custom error messages
if (responseContext.getStatus() != Response.Status.OK.getStatusCode()) {
if (responseContext.hasEntity()) {
// get the "real" error message
ErrorResponse error = _MAPPER.readValue(responseContext.getEntityStream(), ErrorResponse.class);
String message = error.getMessage();
Response.Status status = Response.Status.fromStatusCode(responseContext.getStatus());
WebApplicationException webAppException;
switch (status) {
case BAD_REQUEST:
webAppException = new BadRequestException(message);
break;
case UNAUTHORIZED:
webAppException = new NotAuthorizedException(message);
break;
case FORBIDDEN:
webAppException = new ForbiddenException(message);
break;
case NOT_FOUND:
webAppException = new NotFoundException(message);
break;
case METHOD_NOT_ALLOWED:
webAppException = new NotAllowedException(message);
break;
case NOT_ACCEPTABLE:
webAppException = new NotAcceptableException(message);
break;
case UNSUPPORTED_MEDIA_TYPE:
webAppException = new NotSupportedException(message);
break;
case INTERNAL_SERVER_ERROR:
webAppException = new InternalServerErrorException(message);
break;
case SERVICE_UNAVAILABLE:
webAppException = new ServiceUnavailableException(message);
break;
default:
webAppException = new WebApplicationException(message);
}
throw webAppException;
}
}
}
}
I believe you want to do something like this:
Response response = builder.get( Response.class );
if ( response.getStatusCode() != Response.Status.OK.getStatusCode() ) {
System.out.println( response.getStatusType() );
return null;
}
return response.readEntity( MyEntity.class );
Another thing you can try (since I don't know where this API puts stuff -- i.e. in the header or entity or what) is:
Response response = builder.get( Response.class );
if ( response.getStatusCode() != Response.Status.OK.getStatusCode() ) {
// if they put the custom error stuff in the entity
System.out.println( response.readEntity( String.class ) );
return null;
}
return response.readEntity( MyEntity.class );
If you would like to generally map REST response codes to Java exception you can add a client filter to do that:
class ClientResponseLoggingFilter implements ClientResponseFilter {
#Override
public void filter(final ClientRequestContext reqCtx,
final ClientResponseContext resCtx) throws IOException {
if ( resCtx.getStatus() == Response.Status.BAD_REQUEST.getStatusCode() ) {
throw new MyClientException( resCtx.getStatusInfo() );
}
...
In the above filter you can create specific exceptions for each code or create one generic exception type that wraps the Response code and entity.
There are other ways to getting a custom error message to the Jersey client besides writing a custom filter. (although the filter is an excellent solution)
1) Pass error message in an HTTP header field.
The detail error message could be in the JSON response and in an additional header field, such as "x-error-message".
The Server adds the HTTP error header.
ResponseBuilder rb = Response.status(respCode.getCode()).entity(resp);
if (!StringUtils.isEmpty(errMsg)){
rb.header("x-error-message", errMsg);
}
return rb.build();
The Client catches the exception, NotFoundException in my case, and reads the response header.
try {
Integer accountId = 2222;
Client client = ClientBuilder.newClient();
WebTarget webTarget = client.target("http://localhost:8080/rest-jersey/rest");
webTarget = webTarget.path("/accounts/"+ accountId);
Invocation.Builder ib = webTarget.request(MediaType.APPLICATION_JSON);
Account resp = ib.get(new GenericType<Account>() {
});
} catch (NotFoundException e) {
String errorMsg = e.getResponse().getHeaderString("x-error-message");
// do whatever ...
return;
}
2) Another solution is to catch the exception and read the response content.
try {
// same as above ...
} catch (NotFoundException e) {
String respString = e.getResponse().readEntity(String.class);
// you can convert to JSON or search for error message in String ...
return;
}
The class WebApplicationException was designed for that but for some reason it ignores and overwrites what you specify as parameter for the message.
For that reason I created my own extension WebAppException that honors the parameters. It is a single class and it doesn't require any response filter or a mapper.
I prefer exceptions than creating a Response as it can be thrown from anywhere while processing.
Simple usage:
throw new WebAppException(Status.BAD_REQUEST, "Field 'name' is missing.");
The class:
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.Response.Status.Family;
import javax.ws.rs.core.Response.StatusType;
public class WebAppException extends WebApplicationException {
private static final long serialVersionUID = -9079411854450419091L;
public static class MyStatus implements StatusType {
final int statusCode;
final String reasonPhrase;
public MyStatus(int statusCode, String reasonPhrase) {
this.statusCode = statusCode;
this.reasonPhrase = reasonPhrase;
}
#Override
public int getStatusCode() {
return statusCode;
}
#Override
public Family getFamily() {
return Family.familyOf(statusCode);
}
#Override
public String getReasonPhrase() {
return reasonPhrase;
}
}
public WebAppException() {
}
public WebAppException(int status) {
super(status);
}
public WebAppException(Response response) {
super(response);
}
public WebAppException(Status status) {
super(status);
}
public WebAppException(String message, Response response) {
super(message, response);
}
public WebAppException(int status, String message) {
super(message, Response.status(new MyStatus(status, message)). build());
}
public WebAppException(Status status, String message) {
this(status.getStatusCode(), message);
}
public WebAppException(String message) {
this(500, message);
}
}
A much more concise solution for anyone stumbling on this:
Calling .get(Class<T> responseType) or any of the other methods that take the result type as an argument Invocation.Builder will return a value of the desired type instead of a Response. As a side effect, these methods will check if the received status code is in the 2xx range and throw an appropriate WebApplicationException otherwise.
From the documentation:
Throws: WebApplicationException in case the response status code of
the response returned by the server is not successful and the
specified response type is not Response.
This allows to catch the WebApplicationException, retrieve the actual Response, process the contained entity as exception details (ApiExceptionInfo) and throw an appropriate exception (ApiException).
public <Result> Result get(String path, Class<Result> resultType) {
return perform("GET", path, null, resultType);
}
public <Result> Result post(String path, Object content, Class<Result> resultType) {
return perform("POST", path, content, resultType);
}
private <Result> Result perform(String method, String path, Object content, Class<Result> resultType) {
try {
Entity<Object> entity = null == content ? null : Entity.entity(content, MediaType.APPLICATION_JSON);
return client.target(uri).path(path).request(MediaType.APPLICATION_JSON).method(method, entity, resultType);
} catch (WebApplicationException webApplicationException) {
Response response = webApplicationException.getResponse();
if (response.getMediaType().equals(MediaType.APPLICATION_JSON_TYPE)) {
throw new ApiException(response.readEntity(ApiExceptionInfo.class), webApplicationException);
} else {
throw webApplicationException;
}
}
}
ApiExceptionInfo is custom data type in my application:
import lombok.Data;
#Data
public class ApiExceptionInfo {
private int code;
private String message;
}
ApiException is custom exception type in my application:
import lombok.Getter;
public class ApiException extends RuntimeException {
#Getter
private final ApiExceptionInfo info;
public ApiException(ApiExceptionInfo info, Exception cause) {
super(info.toString(), cause);
this.info = info;
}
}
[At least with Resteasy] there is one big disadvantage with the solution offered by #Chuck M and based on ClientResponseFilter.
When you use it based on ClientResponseFilter, your BadRequestException, NotAuthorizedException, ... exceptions are wrapped by javax.ws.rs.ProcessingException.
Clients of your proxy must not be forced to catch this javax.ws.rs.ResponseProcessingException exception.
Without filter, we get an original rest exception. If we catch and handle by default, it does not give us much:
catch (WebApplicationException e) {
//does not return response body:
e.toString();
// returns null:
e.getCause();
}
The problem can be solved on another level, when you extract a description from the error. WebApplicationException exception, which is a parent for all rest exceptions, contains javax.ws.rs.core.Response. Just write a helper method, that in case the exception is of WebApplicationException type, it will also check the response body. Here is a code in Scala, but the idea should be clear. The methord returns a clear description of the rest exception:
private def descriptiveWebException2String(t: WebApplicationException): String = {
if (t.getResponse.hasEntity)
s"${t.toString}. Response: ${t.getResponse.readEntity(classOf[String])}"
else t.toString
}
Now we move a responsibility to show exact error, on the client. Just use a shared exception handler to minimize effort for clients.
The following works for me
Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();

Categories

Resources