乐闻世界logo
搜索文章和话题

GraphQL 错误处理有哪些最佳实践

2月21日 17:00

GraphQL 错误处理最佳实践

GraphQL 提供了灵活的错误处理机制,但正确实现错误处理对于构建健壮的 API 至关重要。以下是 GraphQL 错误处理的关键策略和最佳实践。

1. GraphQL 错误结构

基本错误响应格式

json
{ "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. 自定义错误类

创建错误类层次结构

javascript
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 中抛出错误

基本错误抛出

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

部分错误处理

javascript
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. 错误格式化

自定义错误格式化器

javascript
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. 错误日志记录

结构化错误日志

javascript
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. 错误恢复策略

降级处理

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

重试机制

javascript
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. 错误类型设计

错误结果类型

graphql
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! }

实现

javascript
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. 错误监控和告警

错误监控集成

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

错误告警

javascript
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使用缓存或降级
标签:GraphQL