I am trying to catch WebApplicationException with my javax.ws.rs.ext.ExceptionMapper class but I get a strange behavior.
This is my simple rest method:
#GET
#Path("/saySomething")
public List<String> saySomething() {
String response = EchoRestClient.ping();
List<String> list = new ArrayList<>();
list.add(response);
list.add("okay");
return list;
}
(1st) This is the client class which calls another rest api:
public class EchoRestClient {
private static Client client = ClientBuilder.newClient();
public static String ping() {
String serviceUrl = PropertyReader.getProperty(ServiceUrl.ECHO_SERVICE);
Response response = client
.target(serviceUrl)
.path("saySomething")
.request(ExtendedMediaType.APPLICATION_UTF8)
.get();
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
return response.getEntity(String.class);
}
throw new WebApplicationException(response);
}
}
And my custom Exception handler, which does NOT catch the above thrown exception:
#Provider
public class WebservletExceptionMapper implements ExceptionMapper<Exception> {
#Override
public Response toResponse(Exception exception) {
System.out.println("caught exception");
Response response;
if (exception instanceof WebApplicationException) {
response = ((WebApplicationException) exception).getResponse();
} else {
response = Response....build();
}
return response;
}
}
(2nd) BUT if I do this the exception is caught (EchoRestClient.java):
public static String ping() {
// same code then before
WebApplicationException e = new WebApplicationException(response);
throw new RuntimeException("xxxxxx", e);
}
My code above works fine and I get a proper response when I call the saySomething rest method from my web browser.
BUT if I undeploy the EchoService rest (contains the called ping rest method) the HTTP 404 is not caught in the 1st case. I need to throw a RuntimeException because WebApplicationException is not caught (2nd case).
According to the documentation the exception hierarchy is WebApplicationException extends RuntimeException.
What is wrong here?
-- EDIT --
If I throw this exception then it is caught fine: throw new WebApplicationException(response.getStatus())
But this one does not work: throw new WebApplicationException(response)
Is something wrong in the response object?
This is caused by an issue in Jersey. If your code throws a WebApplicationException that contains a Response object, ExceptionMappers are not called.
See also:
https://github.com/eclipse-ee4j/jersey/issues/3716
ExceptionMapper not working as expected
Related
I'm using RestTemplate to call my webservice's health actuator endpoint from another webservice of mine to see if the webservice is up. If the webservice is up, all works fine, but when it's down, I get an error 500, "Internal Server Error". If my webservice is down, I'm trying to catch that error to be able to handle it, but the problem I'm having is that I can't seem to be able to catch the error.
I've tried the following and it never enters either of my catch sections
#Service
public class DepositService {
#Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.setConnectTimeout(Duration.ofMillis(3000))
.setReadTimeout(Duration.ofMillis(3000))
.build();
}
private static void getBankAccountConnectorHealth() {
final String uri = "http://localhost:9996/health";
RestTemplate restTemplate = new RestTemplate();
String result = null;
try {
result = restTemplate.getForObject(uri, String.class);
} catch (HttpClientErrorException exception) {
System.out.println("callToRestService Error :" + exception.getResponseBodyAsString());
} catch (HttpStatusCodeException exception) {
System.out.println( "callToRestService Error :" + exception.getResponseBodyAsString());
}
System.out.println(result);
}
}
I've also tried doing it this way, but same results. It never seems to enter my error handler class.
public class NotFoundException extends RuntimeException {
}
public class RestTemplateResponseErrorHandler implements ResponseErrorHandler {
#Override
public boolean hasError(ClientHttpResponse httpResponse) throws IOException {
return (httpResponse.getStatusCode().series() == CLIENT_ERROR || httpResponse.getStatusCode().series() == SERVER_ERROR);
}
#Override
public void handleError(ClientHttpResponse httpResponse) throws IOException {
if (httpResponse.getStatusCode().series() == HttpStatus.Series.SERVER_ERROR) {
// handle SERVER_ERROR
System.out.println("Server error!");
} else if (httpResponse.getStatusCode().series() == HttpStatus.Series.CLIENT_ERROR) {
// handle CLIENT_ERROR
System.out.println("Client error!");
if (httpResponse.getStatusCode() == HttpStatus.NOT_FOUND) {
throw new NotFoundException();
}
}
}
}
#Service
public class DepositService {
#Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.setConnectTimeout(Duration.ofMillis(3000))
.setReadTimeout(Duration.ofMillis(3000))
.build();
}
private static void getAccountHealth() {
final String uri = "http://localhost:9996/health";
RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new RestTemplateResponseErrorHandler());
String result = null;
result = restTemplate.getForObject(uri, String.class);
System.out.println(result);
}
}
Any suggestions as to how I can call my webservice's health actuator from another webservice and catch if that webservice is down?
It looks like the getForObject doesn't throw either of the exceptions you are catching. From the documentation, it throws RestClientException. The best method I have found for identifying thrown exceptions is to catch Exception in the debugger and inspect it to find out if it's useful.
With the second method, I'm not sure why you would create a bean method for the RestTemplate and then create one with new. You probably should inject your RestTemplate and initialise the ResponseErrorHandler with the RestTemplateBuilder::errorHandler method.
Internal serve error throw HttpServerErrorException You should catch this exception if you want to handle it However the better way to do that is using error handler you can see the following posts to see how to do that:
spring-rest-template-error-handling
spring-boot-resttemplate-error-handling
The methods of RestTemplate such as postForEntity() throw RestClientException. I would like to extract the HTTP status code and response body from that exception object in the catch block. How can I do that?
Instead of catching RestClientException, catch the special HttpClientErrorException.
Here's an example:
try {
Link dataCenterLink = serviceInstance.getLink("dataCenter");
String dataCenterUrl = dataCenterLink.getHref();
DataCenterResource dataCenter =
restTemplate.getForObject(dataCenterUrl, DataCenterResource.class);
serviceInstance.setDataCenter(dataCenter);
} catch (HttpClientErrorException e) {
HttpStatus status = e.getStatusCode();
if (status != HttpStatus.NOT_FOUND) { throw e; }
}
HttpClientErrorException provides getStatusCode and getResponseBodyAsByteArray to get the status code and body, respectively.
Catch RestClientResponseException instead. It's more generic.
From the docs: Common base class for exceptions that contain actual HTTP response data.
In some cases, HttpClientErrorException is not thrown. For example the following method restTemplate.exchange call:
ResponseEntity<Employee[]> employees = restTemplate.exchange(url, HttpMethod.GET, entity, Employee[].class);
Gets the http body and marshalls it to an Entity. If remote resource returns a rare error, internal marshall does not work and just a RestClientException is thrown.
restTemplate.setErrorHandler
In this case or if you want to handle any error in restTemplate operations, you could use setErrorHandler. This method receives a basic ResponseErrorHandler with helpful methods.
This method hasError allowed me to get the remote http body text and helped me to detect the error of the invocation or in the remote http remote resource:
restTemplate.setErrorHandler(new ResponseErrorHandler() {
#Override
public boolean hasError(ClientHttpResponse arg0) throws IOException {
System.out.println("StatusCode from remote http resource:"+arg0.getStatusCode());
System.out.println("RawStatusCode from remote http resource:"+arg0.getRawStatusCode());
System.out.println("StatusText from remote http resource:"+arg0.getStatusText());
String body = new BufferedReader(new InputStreamReader(arg0.getBody()))
.lines().collect(Collectors.joining("\n"));
System.out.println("Error body from remote http resource:"+body);
return false;
}
#Override
public void handleError(ClientHttpResponse arg0) throws IOException {
// do something
}
});
Also, you can manually evaluate the body or status and return true or false in order to flag as error or not.
private void sendActivity(StatsActivity statsActivity) throws InterruptedException
{
LibraryConnectorXapiEditView libraryConnectorXapiEditView = (LibraryConnectorXapiEditView) workerBundle.getConnector();
RestTemplate restTemplate = new RestTemplate();
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
Statement statement = libraryConnectorConverter.convertActivityToStatement(statsActivity, workerBundle);
HttpEntity<Statement> request = new HttpEntity<>(statement, headers);
try
{
String lrsEndPoint = libraryConnectorXapiEditView.getLrsEndPoint() + "/statements";
ResponseEntity<String> response = restTemplate.exchange(lrsEndPoint, HttpMethod.POST, request, String.class);
ocnCompletionEventDao.save(this.convertToOcnCompletionEvent(statsActivity, response.getBody(), response.getStatusCodeValue()));
}
catch (HttpClientErrorException ex)
{
ocnCompletionEventDao.save(this.convertToOcnCompletionEvent(statsActivity, ex.getResponseBodyAsString(), ex.getStatusCode().value()));
checkResponse(ex, libraryConnectorXapiEditView);
if(failedAttempts<3)
{
sendActivity(statsActivity);
failedAttempts++;
}
}
}
private void checkResponse(HttpClientErrorException ex, LibraryConnectorXapiEditView libraryConnectorXapiEditView) throws InterruptedException
{
int statusCode = ex.getStatusCode().value();
int retryAfterSeconds = retryAfter(ex.getResponseHeaders());
switch (statusCode)
{
case 401:
headers = xApiAuthorizationUtils.getHeaders(libraryConnectorXapiEditView);
case 429:
if(retryAfterSeconds!=0)
Thread.sleep(retryAfterSeconds);
case 422:
failedAttempts=3;
}
}
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 have implemented rest webservices using Jersey, and whenever some exception occur on the server side, the client gets a generic HTTP 500 Internal Server Error, with no more info of the real exception. I found that people usually catch any exception on the server side, then throws a WebApplicationException, but even this way the client keeps getting the generic HTTP 500 Internal Server Error.
This is my webservice:
#PUT
#Produces(MediaType.APPLICATION_XML)
#Consumes(MediaType.APPLICATION_XML)
#Path("/transmitir")
public WrapperTransmissaoRetorno receber(WrapperTransmissao wrapperRecepcao) {
WrapperTransmissaoRetorno retorno = new WrapperTransmissaoRetorno();
retorno.setCodigoMaster(new Random().nextInt());
retorno.setDataRetorno(new Date());
if(true){
throw new WebApplicationException("Este pau eh bem graudo");
}
return retorno;
}
This is the code that calls the client:
try {
WsTransmissaoCliente client = new WsTransmissaoCliente();
WrapperTransmissao wrapperRecepcao = new WrapperTransmissao();
Transferencia transferencia = new Transferencia();
transferencia.setCodigoTabela(23);
transferencia.setCodigoTransferencia(56);
transferencia.setDataRetorno(new Date());
transferencia.setDataTransmissao(new Date(System.currentTimeMillis()+3000000));
transferencia.setNomeTabela("CUPOM");
transferencia.setTipoOperacao(TipoOperacao.UPDATE);
wrapperRecepcao.setTransferencia(transferencia);
Jumento jumento = new Jumento();
jumento.setIdade(24);
jumento.setNome("José");
wrapperRecepcao.setObjeto(jumento);
// Cabrito cabrito = new Cabrito();
// cabrito.setAltura(56);
// cabrito.setPeso(120.0);
// wrapperRecepcao.setObjeto(cabrito);
WrapperTransmissaoRetorno retorno = client.transmitir(wrapperRecepcao);
System.out.println("Retorno do WS: "+retorno);
} catch (Exception e) {
WebApplicationException exx = (WebApplicationException) e;
exx.printStackTrace();
}
How to avoid this and get the real exception? Or at least the message?
UPDATE
Here is the object i am sending as a response:
package br.atualy.integracaocheckout.wrappers;
import java.util.Date;
import javax.xml.bind.annotation.XmlRootElement;
#XmlRootElement
public class WrapperTransmissaoRetorno {
private Date dataRetorno;
private Integer codigoMaster;
public Date getDataRetorno() {
return dataRetorno;
}
public void setDataRetorno(Date dataRetorno) {
this.dataRetorno = dataRetorno;
}
public Integer getCodigoMaster() {
return codigoMaster;
}
public void setCodigoMaster(Integer codigoMaster) {
this.codigoMaster = codigoMaster;
}
#Override
public String toString() {
return "WrapperRecepcaoRetorno{" + "dataRetorno=" + dataRetorno + ", codigoMaster=" + codigoMaster + '}';
}
}
UPDATE 2
And here is the client:
import br.atualy.integracaocheckout.wrappers.WrapperTransmissao;
import br.atualy.integracaocheckout.wrappers.WrapperTransmissaoRetorno;
import javax.ws.rs.ClientErrorException;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.WebTarget;
public class WsTransmissaoCliente {
private final WebTarget webTarget;
private final Client client;
private static final String BASE_URI = "http://localhost:8080/IntegracaoCheckout/webresources";
public WsTransmissaoCliente() {
client = javax.ws.rs.client.ClientBuilder.newClient();
webTarget = client.target(BASE_URI).path("transmissao");
}
// public String receber() throws ClientErrorException {
// WebTarget resource = webTarget;
// resource = resource.path("receber");
// return resource.request(javax.ws.rs.core.MediaType.APPLICATION_XML).get(String.class);
// }
public WrapperTransmissaoRetorno transmitir(WrapperTransmissao requestEntity) throws ClientErrorException {
return webTarget.path("transmitir")
.request(javax.ws.rs.core.MediaType.APPLICATION_XML)
.put(javax.ws.rs.client.Entity.entity(requestEntity, javax.ws.rs.core.MediaType.APPLICATION_XML), WrapperTransmissaoRetorno.class);
}
public void close() {
client.close();
}
}
If using jawax.ws.rs.core.Response object.
SERVER :: In case of exception/failure set it as :
// do stuff
// here e.getMessage() can be custom failure message too
response = Response.serverError().entity(e.getMessage()).build();
// return response object
return response;
CLIENT :: On the client side check following :
if(response != null && reponse.getStatus() == Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()) {
String serverErrorMsg = response.readEntity(String.class);
throw new Exception(serverErrorMsg);
}
Generally it's better to declare your method as returning a Response object instead of a user-defined type, and set the data as the entity. Then if you want to indicate that an exception has happened, you can just pass that exception as the entity of the Response you are returning.
e.g.
#GET
#Path("/foo")
public Response getFoo() {
try {
// do stuff
return Response.ok(someData).build();
} catch (Exception e) {
return Response.serverError().entity(e).build();
}
}
You'll notice that this way you don't ever end up actually throwing an exception out of your method, but rather return an explicit 500 response with an exception as the entity. This way you can still throw exceptions out of your code, but they'll be handled nicely.
EDIT
I'm not sure what your client wrapper code is doing, but you can pass the expected response data type into your call with the normal REST client:
Client client = ClientBuilder.newClient();
WebTarget target = client.target("http://foo.com/foo");
String response = target.request().get(String.class);
or you can also pull it out of the Response using the readEntity() method:
Client client = ClientBuilder.newClient();
WebTarget target = client.target("http://foo.com/foo");
Response response = target.request().get();
String entity = response.readEntity(String.class);
It sounds like what you need to do is check the return code, and then parse the entity as a either a WrapperTransmissaoRetorno or a WebApplicationException depending on what code was returned:
Response response = client.transmitir(wrapperRecepcao);
if (response.getStatus() == Response.Status.OK.getStatusCode()) { // 200
WrapperTransmissaoRetorno retorno = response.readEntity(WrapperTransmissaoRetorno.class);
// do stuff
} else if (response.getStatus() == Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()) { // 500
WebApplicationException e = response.readEntity(WebApplicationException.class);
// do stuff
} // etc for other response codes
Use response object in webapplication excemption. It should work.
From java docs:
WebApplicationException(String message)
Construct a new instance with a blank message and default HTTP status code of 500.
Its a blank message. I haven't tried it myself. I guess this is the problem.
https://jersey.java.net/apidocs/2.6/jersey/javax/ws/rs/WebApplicationException.html
Even after all the suggestions i could not manage to throw the exception to the client.
So what i did was to put a String property inside my returning class, so when an exception occurs on the server side, this String will contain the exception message and i can get it on the client.
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();