1. 概述

本教程将深入探讨GraphQL中的错误处理机制。首先分析GraphQL规范对错误响应的要求,然后通过Spring Boot实战演示错误处理的最佳实践。对于有经验的开发者来说,理解错误处理是构建健壮GraphQL服务的关键。

2. GraphQL规范中的响应结构

根据GraphQL规范,每个请求必须返回结构化良好的响应。这种响应由成功或失败操作产生的dataerrors映射组成,还可能包含部分成功结果和字段错误。

响应映射的核心组件是errorsdataextensions

  • errors部分描述操作过程中的任何失败。若无错误发生,响应中必须不包含此部分
  • data部分包含成功执行的操作结果:
    • 查询操作时:查询根操作类型的对象
    • 变更操作时:变更根操作类型的对象
    • 若操作因信息缺失、验证错误或语法错误而失败,响应中必须不包含此部分
    • 若操作执行失败,此部分必须为null
  • extensions是可选的映射对象,允许实现者按需添加自定义内容,格式无限制

关键规则:若响应不含data,则必须包含至少一个错误的errors部分。例如违反唯一约束时的错误响应:

mutation {
  addVehicle(vin: "NDXT155NDFTV59834", year: 2021, make: "Toyota", model: "Camry", trim: "XLE",
             location: {zipcode: "75024", city: "Dallas", state: "TX"}) {
    vin
    year
    make
    model
    trim
  }
}
{
  "data": null,
  "errors": [
    {
      "errorType": "DataFetchingException",
      "locations": [
        {
          "line": 2,
          "column": 5,
          "sourceName": null
        }
      ],
      "message": "Failed to add vehicle. Vehicle with vin NDXT155NDFTV59834 already present.",
      "path": [
        "addVehicle"
      ],
      "extensions": {
        "vin": "NDXT155NDFTV59834"
      }
    }
  ]
}

3. GraphQL规范中的错误响应组件

响应中的errors部分是非空错误列表,每个错误都是映射对象。

3.1. 请求错误

这类错误发生在操作执行前,通常由请求本身的问题引起,包括:

  • 请求数据解析失败
  • 请求文档验证错误
  • 不支持的操作
  • 无效的请求值

发生请求错误时,操作尚未开始执行,响应中必须不包含data部分。例如语法错误示例:

query {
  searchByVin(vin: "error) {
    vin
    year
    make
    model
    trim
  }
}

错误响应(缺少引号导致):

{
  "data": null,
  "errors": [
    {
      "message": "Invalid Syntax",
      "locations": [
        {
          "line": 5,
          "column": 8,
          "sourceName": null
        }
      ],
      "errorType": "InvalidSyntax",
      "path": null,
      "extensions": null
    }
  ]
}

3.2. 字段错误

字段错误发生在操作执行期间,通常由以下原因引起:

  • 值无法强制转换为预期类型
  • 特定字段解析时发生内部错误

发生字段错误时,操作会继续执行并返回部分结果,响应中必须同时包含dataerrors部分。例如查询非空字段但实际为空的情况:

query {
  searchAll {
    vin
    year
    make
    model
    trim
  }
}

错误响应(trim字段被标记为非空但存在null值):

{
  "data": {
    "searchAll": [
      null,
      {
        "vin": "JTKKU4B41C1023346",
        "year": 2012,
        "make": "Toyota",
        "model": "Scion",
        "trim": "Xd"
      },
      {
        "vin": "1G1JC1444PZ215071",
        "year": 2000,
        "make": "Chevrolet",
        "model": "CAVALIER VL",
        "trim": "RS"
      }
    ]
  },
  "errors": [
    {
      "message": "Cannot return null for non-nullable type: 'String' within parent 'Vehicle' (/searchAll[0]/trim)",
      "path": [
        "searchAll",
        0,
        "trim"
      ],
      "errorType": "DataFetchingException",
      "locations": null,
      "extensions": null
    }
  ]
}

3.3. 错误响应格式

每个错误必须包含以下关键元素:

必需字段:

  • message:描述失败原因,帮助客户端开发者修正错误

可选字段:

  • locations:错误关联的GraphQL文档位置列表
    "locations": [
      {
        "line": 5,
        "column": 8
      }
    ]
    
  • path:从根元素到错误字段的路径
    "path": [
      "searchAll",
      0,
      "trim"
    ]
    
    • 值为字段名(字符串)或列表索引(数字)
    • 若字段有别名,路径中使用别名

3.4. 字段错误处理规则

处理字段错误时需遵循以下原则:

  1. 基础规则

    • 无论字段是否可空,错误都应视为字段返回null
    • 错误必须添加到errors列表
  2. 可空字段

    • 响应中字段值为null
    • errors中包含描述该字段的错误
  3. 非空字段

    • 错误由父字段处理
    • 若父字段非空,错误会向上传播直到找到可空父字段或根元素
  4. 列表字段

    • 若列表包含非空类型且某个元素为null,整个列表解析为null
    • 若包含该列表的父字段非空,错误同样向上传播
  5. 多重错误

    • 同一字段解析时即使出现多个错误,errors中只添加一个该字段的错误

4. Spring Boot GraphQL库依赖

在Spring Boot应用中,我们使用以下依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-graphql</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.graphql</groupId>
    <artifactId>spring-graphql-test</artifactId>
    <scope>test</scope>
</dependency>
  • spring-boot-starter-graphql:提供核心GraphQL功能
  • spring-graphql-test:用于GraphQL服务测试

5. Spring Boot GraphQL错误处理

本节聚焦Spring Boot应用内的GraphQL错误处理实现(非基础开发知识)。我们将通过车辆查询/变更示例展示不同错误处理方式。

5.1. 标准异常的GraphQL响应

首先创建自定义异常:

public class InvalidInputException extends RuntimeException {
    public InvalidInputException(String message) {
        super(message);
    }
}

默认情况下,GraphQL引擎返回如下通用错误响应:

{
  "errors": [
    {
      "message": "INTERNAL_ERROR for 2c69042a-e7e6-c0c7-03cf-6026b1bbe559",
      "locations": [
        {
          "line": 2,
          "column": 5
        }
      ],
      "path": [
        "searchByLocation"
      ],
      "extensions": {
        "classification": "INTERNAL_ERROR"
      }
    }
  ],
  "data": null
}

默认处理机制

  • ExceptionResolversExceptionHandler处理所有异常
  • 实现DataFetcherExceptionHandler接口
  • 支持注册多个DataFetcherExceptionResolver组件
  • 未解析异常被归类为INTERNAL_ERROR(含执行ID和通用消息)

5.2. 自定义异常的GraphQL响应

创建更具体的异常类:

public class VehicleNotFoundException extends RuntimeException {
    public VehicleNotFoundException(String message) {
        super(message);
    }
}

实现自定义异常解析器(推荐继承DataFetcherExceptionResolverAdapter):

@Component
public class CustomExceptionResolver extends DataFetcherExceptionResolverAdapter {

    @Override
    protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
        if (ex instanceof VehicleNotFoundException) {
            return GraphqlErrorBuilder.newError()
              .errorType(ErrorType.NOT_FOUND)
              .message(ex.getMessage())
              .path(env.getExecutionStepInfo().getPath())
              .location(env.getField().getSourceLocation())
              .build();
        } else {
            return null;
        }
    }
}

现在错误响应将包含明确分类:

{
  "errors": [
    {
      "message": "Vehicle with vin: 123 not found.",
      "locations": [
        {
          "line": 2,
          "column": 5
        }
      ],
      "path": [
        "searchByVin"
      ],
      "extensions": {
        "classification": "NOT_FOUND"
      }
    }
  ],
  "data": {
    "searchByVin": null
  }
}

关键机制

  • 未解析异常以ERROR级别记录(含执行ID)
  • 已解析异常以DEBUG级别记录
  • 通过GraphqlErrorBuilder构建结构化错误

6. 总结

本文系统梳理了GraphQL错误处理的核心概念:

  1. 规范层面

    • 掌握errors/data/extensions响应结构
    • 区分请求错误与字段错误处理逻辑
    • 理解非空字段的错误传播机制
  2. Spring Boot实现

    • 利用DataFetcherExceptionResolver自定义错误处理
    • 通过GraphqlErrorBuilder构建结构化错误响应
    • 合理使用错误分类(如NOT_FOUND/INTERNAL_ERROR

实际开发中,建议为不同业务场景设计专用异常类,通过自定义解析器提供明确的错误分类和上下文信息,避免暴露内部实现细节。完整示例代码可在GitHub获取。


原始标题:Error Handling in GraphQL With Spring Boot