I am using Jackson to interpret JSON responses from an API that I am writing. I would like, as a standard throughout my API, to throw errors from the API to the program with something like:
{"errorMessage":"No such username."}
So I want my response processor to first check if the response is just a single errorMessage key, and if so to handle the error, and if not, then to interpret it as whatever response it was expecting from that command.
So here's my code:
public class ProcessingException extends Exception {
private String errorMessage;
public ProcessingException(){}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
}
and then, in my response handler:
#Override
public void useResponse(InputStream in) throws IOException, ProcessingException {
// turn response into a string
java.util.Scanner s = new java.util.Scanner(in).useDelimiter("\\A");
String response = s.hasNext() ? s.next() : "";
ProcessingException exception;
try {
// Attempt to interpret as an exception
exception = mapper.readValue(response, ProcessingException.class);
}
catch(IOException e) {
// Otherwise interpret it as expected. responseType() is an abstract TypeReference
// which is filled in by subclasses. useResponse() is also abstract. Each subclass
// represents a different kind of request.
Object responseObj = mapper.readValue(response, responseType());
useResponse(responseObj);
return;
}
// I needed this out of the try/catch clause because listener.errorResponse might
// actually choose to throw the passed exception to be dealt with by a higher
// authority.
if (listener!=null) listener.errorResponse(exception);
}
This works beautifully, except under one circumstance - there are some requests that actually don't need to respond with anything, so they return {}. For some reason, this response completely runs through the exception = mapper.readValue(response, ProcessingException.class); line without triggering an IOException, so the program things that there is an error. But then when it tries to read what the error was, it throws a NullPointerException when trying to read exception.getErrorMessage(), because of course there is no error.
Why is it treating {} as a valid ProcessingException object?
Jackson doesn't have bean validation. But what you can do is to declare constructor as a JsonCreator that will be used to instantiate the new object and check/throw an exception in case if that field is null:
class ProcessingException {
private String errorMessage;
#JsonCreator
public ProcessingException(#JsonProperty("errorMessage") String errorMessage) {
if (errorMessage == null) {
throw new IllegalArgumentException("'errorMessage' can't be null");
}
this.errorMessage = errorMessage;
}
// getters, setters and other methods
}
Related
I have a complex object like below
public class TestFilter {
public TestFilter(){
}
private Set<String> m_categories;
private Set<String> m_taskNames;
public Set<String> getCategories() {
return m_categories;
}
public void setCategories(Set<String> categories) {
this.m_categories = categories;
}
public Set<String> getTaskNames() {
return m_taskNames;
}
public void setTaskNames(Set<String> taskNames) {
this.m_taskNames = taskNames;
}
public static TestFilter fromString(String jsonRepresentation){
ObjectMapper mapper = new ObjectMapper();
TestFilter filter= null;
try {
filter = mapper.readValue(jsonRepresentation, TestFilter.class );
} catch (IOException e) {
throw new MyException("Exception while parsing the TestFilter");
}
return filter;
}
}
I am using it in my rest service like below
#GET
#Path("/schedule/info")
public Response getScheduledTasks(#QueryParam(FILTERS)TestFilter testFilter){
if(testFilter == null){
System.out.println("its null");
} else {
System.out.println("not null");
}
return Response.status(Status.OK).entity("Success").build();
}
The url I am using for accessing the object is like below.The url is decoded for ease of reading.
https://myip:port/my-context/rest/schedule/info?filters="{"categories":["C","D"],"taskName":["TaskA"]}"
I had put a debug point on the service, so its hitting the service but the problem is that testFilter is always coming as null.
Please let me know what is the issue with the code.
JSON wrapped in " is just a JSON String. A JSON object should not have the quotes. So just just unwrap the JSON from the quotes
filters={"categories":["C","D"],"taskNames":["TaskA"]}
Another thing, based on your comments. If you want to avoid a 404 NotFound, if you want to change it to something else like 400 Bad Request, then just throw that instead
throw new BadRequestException()
// or new WebApplicationException(400)
It's odd to me that bad query parameters would result in a 404, but this is the specified behavior with JAX-RS. Personally, I just change it to 400 in cases like this.
I have application with many rest services, most of them follows this pattern:
class RestService{
public Response execute1() {
try{
// doLogicThere...
return response;
} catch () {
// handle exception and build Response object..
return response;
}
}
public Response execute2() {
try{
// doLogicThere...
return response;
} catch() {
// handle exception and build Response object..
return response;
}
}
}
catch clause is the same for all methods so I want to have pattern like this below but with try/catch called from somewhere else. I want to do kind of wrapping these methods.
class RestService{
public Response execute1() {
// doLogicThere...
return response;
}
public Response execute2() {
// doLogicThere...
return response;
}
}
JAX-WS includes a mechanism for creating the proper response for each type of exception that your REST methods might produce.
For each exception type, create a class that implements ExceptionMapper<E> where E is the type of exception. You create your response in the toResponse method. You need to annotate your exception mapper with #Provider, in order to register it with the JAX-RS runtime.
#Provider
public class UserNotFoundMapper implements ExceptionMapper<UserNotFoundException> {
#Override
public Response toResponse(UserNotFoundException e) {
return Response.status(404).entity(e.getMessage()).type("text/plain").build();
}
}
You can create an interface with the method you need to execute. Then you can wrap that method in a try catch in a new method. This will avoid the use of many repeated try catch blocks.
You can do something like that:
public interface CallableClass {
public Response call();
}
...
class RestService {
private Response handleCall(CallableClass myClass) {
try {
return myClass.call();
} catch (Exception e) {
// Handle exception and return a particular response
...
}
}
public Response execute1() {
return handleCall(/* put anonymous class of type CallableClass here */);
}
public Response execute2() {
return handleCall(/* put anonymous class of type CallableClass here */);
}
}
If you are using java 8 you can replace the anonynous class with a more elegant lambda expression.
Here a simple example with lambdas
public Response executeLambda() {
return handleCall(() -> {
... // Your code here
return response;
});
}
You can use the "throws" keyword to indicate that a method throws certain exceptions. Then when you call that method you can simply wrap the call in a try/catch block.
See: https://docs.oracle.com/javase/tutorial/essential/exceptions/declaring.html
If you are building RESTFul Service using SPring MVC, you do have a better option of leveraging using following annotation "#ExceptionHandler(CustomExceptionForRestServices.class)". Where you can write your customException or have a comma seperated List of exception classes you expect your method to throw.
The #ExceptionHandler value can be set to an array of Exception types. If an exception is thrown matches one of the types in the list, then the method annotated with the matching #ExceptionHandler will be invoked. If the annotation value is not set then the exception types listed as method arguments are used.
Much like standard controller methods annotated with a #RequestMapping annotation, the method arguments and return values of #ExceptionHandler methods are very flexible.
I have created a little example in plain AspectJ, i.e. without any Spring. I even created a dummy Response class just so as to show the basic mechanics behind aspect-driven exception handling:
Dummy response class:
package de.scrum_master.app;
public class Response {
private int statusCode;
private String message;
public Response(int statusCode) {
this.statusCode = statusCode;
switch (statusCode) {
case 200:
message = "OK";
break;
case 202:
message = "Accepted";
break;
case 401:
message = "Unauthorized";
break;
default:
message = "Unknown status";
}
}
public int getStatusCode() {
return statusCode;
}
#Override
public String toString() {
return "Response(statusCode=" + statusCode + ", message=" + message + ")";
}
}
Driver application with two methods to be intercepted:
As you can see, both methods randomly throw exceptions which ought to be caught by an aspect later.
package de.scrum_master.app;
import java.util.Random;
public class RestService {
private static final Random RANDOM = new Random();
public Response someRequest() {
Response response = new Response(RANDOM.nextBoolean() ? 200 : 401);
if (response.getStatusCode() != 200)
throw new RuntimeException("request failed: " + response);
return response;
}
public Response anotherRequest(String argument) {
Response response = new Response(RANDOM.nextBoolean() ? 200 : 401);
if (response.getStatusCode() != 200)
throw new RuntimeException("request failed: " + response);
return response;
}
public static void main(String[] args) {
RestService restService = new RestService();
for (int i = 0; i < 3; i++) {
System.out.println(restService.someRequest());
System.out.println(restService.anotherRequest("foo"));
}
}
}
Exception handling aspect:
package de.scrum_master.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import de.scrum_master.app.Response;
#Aspect
public class ResponseErrorHandler {
#Around("execution(de.scrum_master.app.Response *(..))")
public Response handleError(ProceedingJoinPoint thisJoinPoint) {
System.out.println("\n" + thisJoinPoint);
try {
return (Response) thisJoinPoint.proceed();
}
catch (Exception e) {
System.out.println(" Handling exception: " + e.getMessage());
return new Response(202);
}
}
}
Console log:
execution(Response de.scrum_master.app.RestService.someRequest())
Response(statusCode=200, message=OK)
execution(Response de.scrum_master.app.RestService.anotherRequest(String))
Response(statusCode=200, message=OK)
execution(Response de.scrum_master.app.RestService.someRequest())
Response(statusCode=200, message=OK)
execution(Response de.scrum_master.app.RestService.anotherRequest(String))
Handling exception: request failed: Response(statusCode=401, message=Unauthorized)
Response(statusCode=202, message=Accepted)
execution(Response de.scrum_master.app.RestService.someRequest())
Response(statusCode=200, message=OK)
execution(Response de.scrum_master.app.RestService.anotherRequest(String))
Handling exception: request failed: Response(statusCode=401, message=Unauthorized)
Response(statusCode=202, message=Accepted)
Feel free to ask follow-up questions if you do not understand the answer.
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);
}
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?
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();