web-dev-qa-db-fra.com

Gestion de la réponse d'erreur personnalisée dans la bibliothèque client JAX-RS 2.0

Je commence à utiliser la nouvelle bibliothèque d'API client dans JAX-RS et je l'aime vraiment jusqu'à présent. J'ai trouvé une chose que je ne peux pas comprendre cependant. L'API que j'utilise a un format de message d'erreur personnalisé qui ressemble à ceci par exemple:

{
    "code": 400,
    "message": "This is a message which describes why there was a code 400."
} 

Il renvoie 400 comme code d'état mais comprend également un message d'erreur descriptif pour vous dire ce que vous avez fait de mal.

Cependant, le client JAX-RS 2.0 remappe le statut 400 en quelque chose de générique et je perds le bon message d'erreur. Il le mappe correctement à une BadRequestException, mais avec un message générique "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)

Existe-t-il une sorte d'intercepteur ou de gestionnaire d'erreur personnalisé qui peut être injecté pour que j'aie accès au vrai message d'erreur. J'ai parcouru la documentation mais je ne vois aucun moyen de le faire.

J'utilise Jersey en ce moment, mais j'ai essayé cela en utilisant CXF et j'ai obtenu le même résultat. Voici à quoi ressemble le code.

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);

METTRE À JOUR:

J'ai implémenté la solution répertoriée dans le commentaire ci-dessous. C'est légèrement différent car les classes ont un peu changé dans l'API client JAX-RS 2.0. Je pense toujours qu'il est faux que le comportement par défaut soit de donner un message d'erreur générique et de rejeter le vrai. Je comprends pourquoi il n'analyserait pas mon objet d'erreur, mais la version non analysée aurait dû être retournée. Je finis par avoir le mappage d'exceptions répliqué que la bibliothèque fait déjà.

Merci pour l'aide.

Voici ma classe de filtre:

@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;
            }
        }
    }
}
38
Chuck M

Je crois que vous voulez faire quelque chose comme ça:

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 );

Une autre chose que vous pouvez essayer (car je ne sais pas où cette API place des choses - c'est-à-dire dans l'en-tête ou l'entité ou quoi) est:

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 );

Si vous souhaitez mapper généralement REST codes de réponse à Java exception, vous pouvez ajouter un filtre client pour ce faire:

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() );
        }

        ...

Dans le filtre ci-dessus, vous pouvez créer des exceptions spécifiques pour chaque code ou créer un type d'exception générique qui encapsule le code de réponse et l'entité.

24
robert_difalco

Il existe d'autres façons d'obtenir un message d'erreur personnalisé pour le client Jersey en plus d'écrire un filtre personnalisé. (bien que le filtre soit une excellente solution)

1) Transmettez le message d'erreur dans un champ d'en-tête HTTP. Le message d'erreur détaillé peut être dans la réponse JSON et dans un champ d'en-tête supplémentaire, tel que "x-error-message".

Server ajoute l'en-tête d'erreur HTTP.

ResponseBuilder rb = Response.status(respCode.getCode()).entity(resp);
if (!StringUtils.isEmpty(errMsg)){
    rb.header("x-error-message", errMsg);
}
return rb.build();

Le Client intercepte l'exception, NotFoundException dans mon cas, et lit l'en-tête de réponse.

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) Une autre solution consiste à intercepter l'exception et à lire le contenu de la réponse.

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;
} 
5
Peter Tarlos

La classe WebApplicationException a été conçue pour cela mais pour une raison quelconque, elle ignore et écrase ce que vous spécifiez comme paramètre pour le message.

Pour cette raison, j'ai créé ma propre extension WebAppException qui respecte les paramètres. Il s'agit d'une seule classe et elle ne nécessite aucun filtre de réponse ni mappeur.

Je préfère les exceptions à la création d'un Response car il peut être lancé de n'importe où lors du traitement.

Utilisation simple:

throw new WebAppException(Status.BAD_REQUEST, "Field 'name' is missing.");

La classe:

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);
    }

}
3
Pla

Ce qui suit fonctionne pour moi

Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
0
Shivoam

Une solution beaucoup plus concise pour quiconque trébuche sur ce sujet:

Appel de .get(Class<T> responseType) ou de l'une des autres méthodes qui prennent le type de résultat comme argument Invocation.Builder renverra une valeur du type souhaité au lieu d'un Response. En tant qu'effet secondaire, ces méthodes vérifieront si le code d'état reçu est dans la plage 2xx et lancera un WebApplicationException approprié sinon.

De la documentation :

Lance: WebApplicationException au cas où le code d'état de réponse de la réponse retournée par le serveur échoue et le type de réponse spécifié n'est pas Response.

Cela permet d'attraper le WebApplicationException, de récupérer le Response réel, de traiter l'entité contenue comme détails d'exception (ApiExceptionInfo) et de lever une exception appropriée (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 est un type de données personnalisé dans mon application:

import lombok.Data;

@Data
public class ApiExceptionInfo {

    private int code;

    private String message;

}

ApiException est un type d'exception personnalisé dans mon 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;
    }

}
0
toKrause

[Au moins avec Resteasy] il y a un gros inconvénient avec la solution offerte par @Chuck M et basée sur ClientResponseFilter.

Lorsque vous l'utilisez en fonction de ClientResponseFilter, vos BadRequestException, NotAuthorizedException, ... exceptions sont encapsulées par javax.ws.rs.ProcessingException.

Les clients de votre proxy ne doivent pas être obligés d'attraper ce javax.ws.rs.ResponseProcessingException exception.

Sans filtre, nous obtenons une exception de repos d'origine. Si nous attrapons et gérons par défaut, cela ne nous donne pas grand-chose:

catch (WebApplicationException e) {
 //does not return response body:
 e.toString();
 // returns null:
 e.getCause();
}

Le problème peut être résolu à un autre niveau, lorsque vous extrayez une description de l'erreur.WebApplicationException exception, qui est un parent pour toutes les exceptions de repos, contient javax.ws.rs.core .Réponse. Écrivez simplement une méthode d'assistance, au cas où l'exception serait de type WebApplicationException, elle vérifierait également le corps de la réponse. Voici un code en Scala, mais l'idée doit être claire. La méthord renvoie une description claire de l'exception restante:

  private def descriptiveWebException2String(t: WebApplicationException): String = {
    if (t.getResponse.hasEntity)
      s"${t.toString}. Response: ${t.getResponse.readEntity(classOf[String])}"
    else t.toString
  }

Maintenant, nous déplaçons la responsabilité d'afficher l'erreur exacte sur le client. Utilisez simplement un gestionnaire d'exceptions partagé pour minimiser les efforts des clients.

0
Alexandr