【问题标题】:GraphQL java send custom error in json formatGraphQL java以json格式发送自定义错误
【发布时间】:2019-02-04 15:00:51
【问题描述】:

我在一个 graphql 应用程序中工作,我必须在 json 中发送自定义错误对象/消息,无论它发生在 servlet 还是服务中。

预期的错误响应

{ errorCode: 400 //error goes here, errorMessage: "my error mesage"}

如果有人可以指导我实现上述要求,将会很有帮助。

【问题讨论】:

    标签: json error-handling graphql graphql-java


    【解决方案1】:

    GraphQL specification 为响应中的error 条目定义了清晰的格式。

    根据规范,应该是这样的(假设使用JSON格式):

      "errors": [
        {
          "message": "Name for character with ID 1002 could not be fetched.",
          "locations": [ { "line": 6, "column": 7 } ],
          "path": [ "hero", "heroFriends", 1, "name" ]
          "extensions": {/* You can place data in any format here */}
        }
      ]
    

    所以你不会找到允许你扩展它并在 GraphQL 执行结果中返回类似这样的 GraphQL 实现,例如:

      "errors": [
        {
          "errorMessage": "Name for character with ID 1002 could not be fetched.",
          "errorCode": 404
        }
      ]
    

    但是,规范允许您在 extension 条目中添加任何格式的数据。因此,您可以在服务器端创建一个自定义异常,并最终得到如下 JSON 响应:

      "errors": [
        {
          "message": "Name for character with ID 1002 could not be fetched.",
          "locations": [ { "line": 6, "column": 7 } ],
          "path": [ "hero", "heroFriends", 1, "name" ]
          "extensions": {
              "errorMessage": "Name for character with ID 1002 could not be fetched.",
              "errorCode": 404
          }
        }
      ]
    

    在 GraphQL Java 上实现这一点非常容易,如 the docs 中所述。您可以创建一个覆盖getExtensions 方法的自定义异常,并在实现中创建一个映射,然后将用于构建extensions 的内容:

    public class CustomException extends RuntimeException implements GraphQLError {
        private final int errorCode;
    
        public CustomException(int errorCode, String errorMessage) {
            super(errorMessage);
    
            this.errorCode = errorCode;
        }
    
        @Override
        public Map<String, Object> getExtensions() {
            Map<String, Object> customAttributes = new LinkedHashMap<>();
    
            customAttributes.put("errorCode", this.errorCode);
            customAttributes.put("errorMessage", this.getMessage());
    
            return customAttributes;
        }
    
        @Override
        public List<SourceLocation> getLocations() {
            return null;
        }
    
        @Override
        public ErrorType getErrorType() {
            return null;
        }
    }
    

    然后您可以从数据提取器中抛出异常,并在代码和消息中传递:

    throw new CustomException(400, "A custom error message");
    

    现在,还有另一种方法可以解决这个问题。

    假设您正在开发一个 Web 应用程序,您可以以您想要的任何格式返回错误(和数据)。虽然在我看来这有点尴尬。 GraphQL 客户端(如 Apollo)遵守规范,那么为什么要返回任何其他格式的响应呢?但无论如何,那里有很多不同的要求。

    获得ExecutionResult 后,您可以创建任意格式的地图或对象,将其序列化为 JSON 并通过 HTTP 返回。

    Map<String, Object> result = new HashMap<>();
    
    result.put("data", executionResult.getData());
    
    List<Map<String, Object>> errors = executionResult.getErrors()
            .stream()
            .map(error -> {
                Map<String, Object> errorMap = new HashMap<>();
    
                errorMap.put("errorMessage", error.getMessage());
                errorMap.put("errorCode", 404); // get the code somehow from the error object
    
                return errorMap;
            })
            .collect(toList());
    
    result.put("errors", errors);
    
    // Serialize "result" and return that.
    

    但是,在大多数情况下,不符合规范的响应是没有意义的。

    【讨论】:

    • 感谢您的回复。对我来说,它现在适用于自定义错误。但是我怎样才能连接其他错误,例如 NonNullableValueCoercedAsNullException ?该异常是由 graphql(Apollo) 引发的。
    • @pipo_dev 这对我不起作用。 CustomException 被 graphql.servlet.DefaultGraphQLErrorHandler 忽略。您还有其他解决方法吗?
    • 答案中给出的字段名称是extension,但the spec,答案中的其他代码说它应该是extensions。不幸的是,我无权进行单字符编辑:)
    【解决方案2】:

    另一个发布的答案对我不起作用。 我通过创建以下类找到了解决方案:

    1) GraphQLError 类型的可投掷 CustomException(就像另一个答案中提到的那样)。

    2) 创建一个GraphQLError 适配器,它不是Throwable。

    3) 自定义GraphQLErrorHandler 用于过滤自定义异常。

    第 1 步:
    以下可抛出的CustomGraphQLException 实现了GraphQLError,因为GraphQLErrorHandler 接口只接受GraphQLError 类型的错误。

    public class CustomGraphQLException extends RuntimeException implements GraphQLError {
    
        private final int errorCode;
        private final String errorMessage;
    
        public CustomGraphQLException(int errorCode, String errorMessage) {
            super(errorMessage);
            this.errorCode = errorCode;
            this.errorMessage = errorMessage;
        }
    
        @Override
        public List<SourceLocation> getLocations() {
            return null;
        }
    
        @Override
        public ErrorType getErrorType() {
            return null;
        }
    
        @Override
        public String getMessage() {
            return this.errorMessage;
        }
    
        @Override
        public Map<String, Object> getExtensions() {
            Map<String, Object> customAttributes = new HashMap<>();
            customAttributes.put("errorCode", this.errorCode);
            customAttributes.put("errorMessage", this.getMessage());
            return customAttributes;
        }
    }
    

    第 2 步:
    创建了一个不可抛出的适配器 GraphQLError 以避免在最终的 GraphQL 错误响应中传递上述自定义异常的堆栈跟踪。

    public class GraphQLErrorAdaptor implements GraphQLError {
    
        private final GraphQLError graphQLError;
    
        public GraphQLErrorAdaptor(GraphQLError graphQLError) {
            this.graphQLError = graphQLError;
        }
    
        @Override
        public List<SourceLocation> getLocations() {
            return graphQLError.getLocations();
        }
    
        @Override
        public ErrorType getErrorType() {
            return graphQLError.getErrorType();
        }
    
        @Override
        public String getMessage() {
            return graphQLError.getMessage();
        }
    
        @Override
        public Map<String, Object> getExtensions() {
            return graphQLError.getExtensions();
        }
    }
    

    第 3 步:
    实现了自定义 GraphQLErrorHandler 以过滤自定义 CustomGraphQLException 并避免将其替换为默认的 graphQL 错误响应。

    public class CustomGraphQLErrorHandler implements GraphQLErrorHandler {
    
        public CustomGraphQLErrorHandler() { }
    
        public List<GraphQLError> processErrors(List<GraphQLError> errors) {
            List<GraphQLError> clientErrors = this.filterGraphQLErrors(errors);
            List<GraphQLError> internalErrors = errors.stream()
                    .filter(e -> isInternalError(e))
                    .map(GraphQLErrorAdaptor::new)
                    .collect(Collectors.toList());
    
            if (clientErrors.size() + internalErrors.size() < errors.size()) {
                clientErrors.add(new GenericGraphQLError("Internal Server Error(s) while executing query"));
                errors.stream().filter((error) -> !this.isClientError(error)
                ).forEach((error) -> {
                    if (error instanceof Throwable) {
                        LOG.error("Error executing query!", (Throwable) error);
                    } else {
                        LOG.error("Error executing query ({}): {}", error.getClass().getSimpleName(), error.getMessage());
                    }
    
                });
            }
            List<GraphQLError> finalErrors = new ArrayList<>();
            finalErrors.addAll(clientErrors);
            finalErrors.addAll(internalErrors);
    
            return finalErrors;
        }
    
        protected List<GraphQLError> filterGraphQLErrors(List<GraphQLError> errors) {
            return errors.stream().filter(this::isClientError).collect(Collectors.toList());
        }
    
        protected boolean isClientError(GraphQLError error) {
            return !(error instanceof ExceptionWhileDataFetching) && !(error instanceof Throwable);
        }
    
        protected boolean isInternalError(GraphQLError error) {
            return (error instanceof ExceptionWhileDataFetching) &&
                    (((ExceptionWhileDataFetching) error).getException() instanceof CustomGraphQLException);
        }
    
    }
    

    第 4 步: 在GraphQLServlet 中配置CustomGraphQLErrorHandler。我假设您在此步骤中使用 spring-boot。

    @Configuration
    public class GraphQLConfig {
    
        @Bean
        public ServletRegistrationBean graphQLServletRegistrationBean(
                QueryResolver queryResolver,
                CustomGraphQLErrorHandler customGraphQLErrorHandler) throws Exception {
    
            GraphQLSchema schema = SchemaParser.newParser()
                    .schemaString(IOUtils.resourceToString("/library.graphqls", Charset.forName("UTF-8")))
                    .resolvers(queryResolver)
                    .build()
                    .makeExecutableSchema();
    
            return new ServletRegistrationBean(new SimpleGraphQLServlet(schema,
                    new DefaultExecutionStrategyProvider(), null, null, null,
                    customGraphQLErrorHandler, new DefaultGraphQLContextBuilder(), null,
                    null), "/graphql");
    
        }
    
    }
    

    Reference

    【讨论】:

    • SimpleGraphQLServlet 已弃用
    • 我已经尝试在 Kotlin 中实现它,但是当我实现 GraphQLError 并从 RuntimeException 继承时出现错误。我收到一条消息说“意外覆盖:以下声明具有相同的 JVM 签名”。这是关于覆盖getMessage()。关于我能做些什么来解决它的任何想法?谢谢。
    • @AdlyThebaud 我猜这是 Kotlin 的一个已知问题,当您尝试覆盖一些用 java 编写的类时。请参阅此 - youtrack.jetbrains.com/issue/KT-6653#comment=27-2666539。作为一种解决方法,您可以在 java 中编写该特定类,并让所有其他代码在 Kotlin 中。它将解决您的问题。
    • @SahilChhabra 谢谢。我的一位同事指出,我可以用@Suppress("ACCIDENTAL_OVERRIDE") 注释函数getMessage()。就目前而言,这似乎奏效了。
    • 有谁知道如何获取查询的位置和路径并在错误响应中返回?
    猜你喜欢
    • 2019-06-28
    • 1970-01-01
    • 2019-02-07
    • 2020-11-09
    • 2015-04-11
    • 1970-01-01
    • 2015-07-14
    • 2013-08-08
    • 1970-01-01
    相关资源
    最近更新 更多