Подтвердить что ты не робот

Обработка пользовательских ответов на ошибки в клиентской библиотеке 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;
            }
        }
    }
}
4b9b3361

Ответ 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
  }

Теперь мы берем на себя ответственность показывать точную ошибку на клиенте. Просто используйте общий обработчик исключений, чтобы минимизировать усилия для клиентов.