Обработка пользовательских ответов на ошибки в клиентской библиотеке JAX-RS 2.0
Я начинаю использовать новую клиентскую библиотеку API в JAX-RS и действительно люблю ее до сих пор. Однако я нашел одно, чего не могу понять. API, который я использую, имеет собственный формат сообщения об ошибке, который выглядит следующим образом:
{
"code": 400,
"message": "This is a message which describes why there was a code 400."
}
Он возвращает 400 в качестве кода состояния, но также содержит описательное сообщение об ошибке, чтобы сообщить вам, что вы сделали неправильно.
Однако клиент JAX-RS 2.0 повторно отображает статус 400 во что-то общее, и я теряю хорошее сообщение об ошибке. Он правильно отображает его в BadRequestException, но с общим сообщением "HTTP 400 Bad Request".
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)
Есть ли какой-то перехватчик или настраиваемый обработчик ошибок, которые могут быть введены, чтобы я получил доступ к реальному сообщению об ошибке. Я просматривал документацию, но не вижу никакого способа сделать это.
Я использую Джерси прямо сейчас, но я попробовал это с помощью CXF и получил тот же результат. Вот как выглядит код.
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:
Я реализовал решение, указанное в комментарии ниже. Это немного отличается, поскольку классы немного изменились в клиентском API JAX-RS 2.0. Я по-прежнему считаю неправильным, что поведение по умолчанию - дать общее сообщение об ошибке и отказаться от реального. Я понимаю, почему он не будет анализировать мой объект ошибки, но необработанная версия должна быть возвращена. В итоге я получаю репликационное отображение исключений, которое уже делает библиотека.
Спасибо за помощь.
Вот мой класс фильтра:
@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;
}
}
}
}
Ответы
Ответ 1
Я считаю, что вы хотите сделать что-то вроде этого:
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 );
Еще одна вещь, которую вы можете попробовать (так как я не знаю, где этот API помещает материал, т.е. в заголовок или сущность или что):
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 );
Если вы хотите в целом сопоставить коды ответов REST с исключением Java, вы можете добавить к нему клиентский фильтр:
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() );
}
...
В приведенном выше фильтре вы можете создавать определенные исключения для каждого кода или создавать один общий тип исключения, который обертывает код ответа и объект.
Ответ 2
Существуют другие способы получения пользовательского сообщения об ошибке клиенту Джерси, помимо написания настраиваемого фильтра. (хотя фильтр является отличным решением)
1) Пропустите сообщение об ошибке в поле заголовка HTTP.
Сообщение об ошибке детали может быть в ответе JSON и в дополнительном поле заголовка, например "x-error-message".
Сервер добавляет заголовок ошибки HTTP.
ResponseBuilder rb = Response.status(respCode.getCode()).entity(resp);
if (!StringUtils.isEmpty(errMsg)){
rb.header("x-error-message", errMsg);
}
return rb.build();
Клиент ловит исключение, NotFoundException в моем случае, и читает заголовок ответа.
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) Еще одно решение - поймать исключение и прочитать содержимое ответа.
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;
}
Ответ 3
Класс WebApplicationException был разработан для этого, но по какой-то причине он игнорирует и перезаписывает то, что вы указываете в качестве параметра для сообщения.
По этой причине я создал собственное расширение WebAppException
которое WebAppException
параметры. Это отдельный класс, и он не требует никакого фильтра ответа или преобразователя.
Я предпочитаю исключения, чем создание Response
как он может генерироваться из любого места во время обработки.
Простое использование:
throw new WebAppException(Status.BAD_REQUEST, "Field 'name' is missing.");
Класс:
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);
}
}
Ответ 4
Следующие работы для меня
Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
Ответ 5
Более сжатое решение для тех, кто наткнулся на это:
Вызов .get(Class<T> responseType)
или любой другой метод, который принимает тип результата в качестве аргумента Invocation.Builder
, вернет значение желаемого типа вместо Response
. В качестве побочного эффекта эти методы будут проверять, находится ли полученный код состояния в диапазоне 2xx и в противном случае выбрасывает соответствующий WebApplicationException
.
Из документация:
Броски: исключение WebApplicationException в случае, если код состояния ответа ответ, возвращаемый сервером, не будет успешным, и указанный тип ответа не Ответ.
Это позволяет поймать WebApplicationException
, получить фактический Response
, обработать содержащуюся сущность в качестве данных о точках исключения (ApiExceptionInfo
) и выбросить соответствующее исключение (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
- это пользовательский тип данных в моем приложении:
import lombok.Data;
@Data
public class ApiExceptionInfo {
private int code;
private String message;
}
ApiException
является настраиваемым типом исключения в моем приложении:
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;
}
}
Ответ 6
[По крайней мере, с Resteasy] есть один большой недостаток решения, предложенного @Chuck M и основанного на ClientResponseFilter
.
Когда вы используете его на основе BadRequestException
, NotAuthorizedException
исключения BadRequestException
, NotAuthorizedException
,... заключаются в javax.ws.rs.ProcessingException
.
Клиенты вашего прокси не должны быть вынуждены перехватывать это исключение javax.ws.rs.ResponseProcessingException
.
Без фильтра мы получаем оригинальное исключение отдыха. Если мы поймаем и обработаем по умолчанию, это не даст нам много:
catch (WebApplicationException e) {
//does not return response body:
e.toString();
// returns null:
e.getCause();
}
Проблема может быть решена на другом уровне, когда вы извлекаете описание из ошибки. Исключение WebApplicationException
, которое является родительским для всех остальных исключений, содержит javax.ws.rs.core.Response. Просто напишите вспомогательный метод, который в случае, если исключение имеет тип WebApplicationException
, также проверит тело ответа. Вот код в Scala, но идея должна быть ясной. Меторд возвращает четкое описание остальных исключений:
private def descriptiveWebException2String(t: WebApplicationException): String = {
if (t.getResponse.hasEntity)
s"${t.toString}. Response: ${t.getResponse.readEntity(classOf[String])}"
else t.toString
}
Теперь мы берем на себя ответственность показывать точную ошибку на клиенте. Просто используйте общий обработчик исключений, чтобы минимизировать усилия для клиентов.