GraphQL 错误处理最佳实践
GraphQL 提供了灵活的错误处理机制,但正确实现错误处理对于构建健壮的 API 至关重要。以下是 GraphQL 错误处理的关键策略和最佳实践。
1. GraphQL 错误结构
基本错误响应格式
{
"data": {
"user": null
},
"errors": [
{
"message": "User not found",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": ["user"],
"extensions": {
"code": "NOT_FOUND",
"timestamp": "2024-01-01T12:00:00Z"
}
}
]
}
错误字段说明
- message: 错误描述信息
- locations: 错误在查询中的位置
- path: 错误发生的字段路径
- extensions: 自定义扩展信息
2. 自定义错误类
创建错误类层次结构
class GraphQLError extends Error {
constructor(message, code, extensions = {}) {
super(message);
this.name = 'GraphQLError';
this.code = code;
this.extensions = extensions;
}
}
class ValidationError extends GraphQLError {
constructor(message, field) {
super(message, 'VALIDATION_ERROR', { field });
}
}
class NotFoundError extends GraphQLError {
constructor(resource, id) {
super(`${resource} with id ${id} not found`, 'NOT_FOUND', { resource, id });
}
}
class AuthenticationError extends GraphQLError {
constructor(message = 'Authentication required') {
super(message, 'AUTHENTICATION_ERROR');
}
}
class AuthorizationError extends GraphQLError {
constructor(message = 'Not authorized') {
super(message, 'AUTHORIZATION_ERROR');
}
}
class ConflictError extends GraphQLError {
constructor(message) {
super(message, 'CONFLICT_ERROR');
}
}
class RateLimitError extends GraphQLError {
constructor(retryAfter) {
super('Rate limit exceeded', 'RATE_LIMIT_ERROR', { retryAfter });
}
}
3. 在 Resolver 中抛出错误
基本错误抛出
const resolvers = {
Query: {
user: async (_, { id }) => {
const user = await User.findById(id);
if (!user) {
throw new NotFoundError('User', id);
}
return user;
}
},
Mutation: {
createUser: async (_, { input }) => {
// 验证输入
if (!input.email || !isValidEmail(input.email)) {
throw new ValidationError('Invalid email address', 'email');
}
// 检查用户是否已存在
const existingUser = await User.findByEmail(input.email);
if (existingUser) {
throw new ConflictError('User with this email already exists');
}
return User.create(input);
}
}
};
部分错误处理
const resolvers = {
Mutation: {
createUsers: async (_, { inputs }) => {
const results = [];
const errors = [];
for (const input of inputs) {
try {
const user = await User.create(input);
results.push({ success: true, user });
} catch (error) {
errors.push({
success: false,
input,
error: error.message
});
}
}
return {
results,
errors,
total: inputs.length,
successCount: results.length,
failureCount: errors.length
};
}
}
};
4. 错误格式化
自定义错误格式化器
const formatError = (error) => {
// 处理自定义错误
if (error.originalError instanceof GraphQLError) {
return {
message: error.message,
code: error.originalError.code,
extensions: error.originalError.extensions
};
}
// 处理验证错误
if (error.originalError instanceof ValidationError) {
return {
message: error.message,
code: 'VALIDATION_ERROR',
field: error.originalError.field
};
}
// 生产环境不暴露详细错误
if (process.env.NODE_ENV === 'production') {
return {
message: 'Internal server error',
code: 'INTERNAL_SERVER_ERROR'
};
}
// 开发环境返回完整错误信息
return {
message: error.message,
code: 'INTERNAL_SERVER_ERROR',
stack: error.stack
};
};
const server = new ApolloServer({
typeDefs,
resolvers,
formatError
});
5. 错误日志记录
结构化错误日志
const winston = require('winston');
const logger = winston.createLogger({
level: 'error',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log' })
]
});
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
{
requestDidStart: () => ({
didEncounterErrors: (context) => {
context.errors.forEach((error) => {
logger.error('GraphQL Error', {
message: error.message,
code: error.extensions?.code,
path: error.path,
locations: error.locations,
query: context.request.query,
variables: context.request.variables
});
});
}
})
}
]
});
6. 错误恢复策略
降级处理
const resolvers = {
Query: {
userProfile: async (_, { id }, { dataSources }) => {
try {
// 尝试从主数据源获取
return await dataSources.userAPI.getUser(id);
} catch (error) {
// 如果主数据源失败,使用缓存数据
const cachedUser = await redis.get(`user:${id}`);
if (cachedUser) {
logger.warn('Using cached user data due to API failure', { userId: id });
return JSON.parse(cachedUser);
}
// 如果缓存也没有,返回默认数据
logger.error('Failed to fetch user data', { userId: id, error });
return {
id,
name: 'Unknown User',
isFallback: true
};
}
}
}
};
重试机制
async function retryOperation(operation, maxRetries = 3, delay = 1000) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
return await operation();
} catch (error) {
lastError = error;
// 如果是可重试的错误,等待后重试
if (isRetryableError(error) && i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
continue;
}
throw error;
}
}
throw lastError;
}
function isRetryableError(error) {
const retryableCodes = ['NETWORK_ERROR', 'TIMEOUT', 'SERVICE_UNAVAILABLE'];
return retryableCodes.includes(error.code);
}
const resolvers = {
Query: {
externalData: async () => {
return retryOperation(async () => {
return await externalAPI.fetchData();
});
}
}
};
7. 错误类型设计
错误结果类型
type Error {
code: String!
message: String!
field: String
details: String
}
type UserResult {
user: User
errors: [Error!]!
success: Boolean!
}
type Mutation {
createUser(input: CreateUserInput!): UserResult!
updateUser(id: ID!, input: UpdateUserInput!): UserResult!
}
实现
const resolvers = {
Mutation: {
createUser: async (_, { input }) => {
const errors = [];
// 验证输入
if (!input.name) {
errors.push({
code: 'REQUIRED_FIELD',
message: 'Name is required',
field: 'name'
});
}
if (!input.email) {
errors.push({
code: 'REQUIRED_FIELD',
message: 'Email is required',
field: 'email'
});
} else if (!isValidEmail(input.email)) {
errors.push({
code: 'INVALID_EMAIL',
message: 'Invalid email format',
field: 'email'
});
}
// 如果有错误,返回错误信息
if (errors.length > 0) {
return {
user: null,
errors,
success: false
};
}
// 创建用户
try {
const user = await User.create(input);
return {
user,
errors: [],
success: true
};
} catch (error) {
return {
user: null,
errors: [{
code: 'INTERNAL_ERROR',
message: 'Failed to create user',
details: error.message
}],
success: false
};
}
}
}
};
8. 错误监控和告警
错误监控集成
const Sentry = require('@sentry/node');
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV
});
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
{
requestDidStart: () => ({
didEncounterErrors: (context) => {
context.errors.forEach((error) => {
// 发送错误到 Sentry
Sentry.captureException(error, {
tags: {
graphql: true,
operation: context.request.operationName
},
extra: {
query: context.request.query,
variables: context.request.variables
}
});
});
}
})
}
]
});
错误告警
const alertThreshold = {
errorRate: 0.05, // 5% 错误率
errorCount: 100 // 100 个错误
};
let errorCount = 0;
let requestCount = 0;
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
{
requestDidStart: () => ({
willSendResponse: (context) => {
requestCount++;
if (context.response.errors && context.response.errors.length > 0) {
errorCount += context.response.errors.length;
// 检查是否需要告警
const errorRate = errorCount / requestCount;
if (errorRate > alertThreshold.errorRate ||
errorCount > alertThreshold.errorCount) {
sendAlert({
message: 'High error rate detected',
errorRate,
errorCount,
requestCount
});
}
}
}
})
}
]
});
9. 错误处理最佳实践总结
| 实践 | 说明 |
|---|
| 使用自定义错误类 | 创建清晰的错误层次结构 |
| 提供详细的错误信息 | 包含错误代码、消息和上下文 |
| 部分错误处理 | 允许部分成功的操作 |
| 错误格式化 | 统一错误响应格式 |
| 错误日志记录 | 记录所有错误用于分析 |
| 错误恢复 | 实现降级和重试机制 |
| 错误监控 | 实时监控错误率 |
| 错误告警 | 及时通知团队 |
10. 常见错误场景及处理
| 场景 | 错误类型 | 处理方式 |
|---|
| 资源不存在 | NotFoundError | 返回 404 错误 |
| 验证失败 | ValidationError | 返回字段级错误 |
| 认证失败 | AuthenticationError | 返回 401 错误 |
| 授权失败 | AuthorizationError | 返回 403 错误 |
| 数据冲突 | ConflictError | 返回 409 错误 |
| 速率限制 | RateLimitError | 返回 429 错误 |
| 网络错误 | NetworkError | 重试或降级 |
| 服务不可用 | ServiceUnavailableError | 使用缓存或降级 |