واجهات GraphQL
معالجة الأخطاء في GraphQL
فهم معالجة الأخطاء في GraphQL
يحتوي GraphQL على تنسيق خطأ موحد يسمح بتسليم البيانات الجزئية جنبًا إلى جنب مع الأخطاء. على عكس واجهات REST API التي ترجع أكواد حالة HTTP، تُرجع استجابات GraphQL دائمًا 200 OK وتتضمن الأخطاء في نص الاستجابة.
تنسيق خطأ GraphQL
يتضمن هيكل خطأ GraphQL القياسي:
{
"data": {
"user": null
},
"errors": [
{
"message": "User not found",
"locations": [{"line": 2, "column": 3}],
"path": ["user"],
"extensions": {
"code": "USER_NOT_FOUND",
"timestamp": "2026-02-16T10:30:00Z"
}
}
]
}
ملاحظة: يمكن أن يحتوي حقل
data على نتائج جزئية حتى عند حدوث أخطاء، مما يسمح للعملاء باستخدام أي بيانات تم حلها بنجاح.
فئات الأخطاء المخصصة
إنشاء فئات أخطاء مخصصة لمعالجة أفضل للأخطاء:
// errors/CustomErrors.js
class AuthenticationError extends Error {
constructor(message) {
super(message);
this.extensions = {
code: 'UNAUTHENTICATED',
http: { status: 401 }
};
}
}
class ForbiddenError extends Error {
constructor(message) {
super(message);
this.extensions = {
code: 'FORBIDDEN',
http: { status: 403 }
};
}
}
class ValidationError extends Error {
constructor(message, fields) {
super(message);
this.extensions = {
code: 'BAD_USER_INPUT',
fields: fields,
http: { status: 400 }
};
}
}
class NotFoundError extends Error {
constructor(message) {
super(message);
this.extensions = {
code: 'NOT_FOUND',
http: { status: 404 }
};
}
}
module.exports = {
AuthenticationError,
ForbiddenError,
ValidationError,
NotFoundError
};
استخدام الأخطاء المخصصة في المحللات
const { AuthenticationError, NotFoundError } = require('./errors/CustomErrors');
const resolvers = {
Query: {
user: async (_, { id }, context) => {
if (!context.user) {
throw new AuthenticationError('You must be logged in');
}
const user = await User.findById(id);
if (!user) {
throw new NotFoundError(`User with ID ${id} not found`);
}
return user;
}
},
Mutation: {
updateUser: async (_, { id, input }, context) => {
if (!context.user) {
throw new AuthenticationError('Authentication required');
}
if (context.user.id !== id && !context.user.isAdmin) {
throw new ForbiddenError('You can only update your own profile');
}
return await User.findByIdAndUpdate(id, input, { new: true });
}
}
};
إخفاء الأخطاء والأمان
إخفاء الأخطاء الداخلية في الإنتاج لمنع تسرب المعلومات:
const { ApolloServer } = require('apollo-server');
const server = new ApolloServer({
typeDefs,
resolvers,
formatError: (error) => {
// تسجيل الخطأ الأصلي
console.error(error);
// لا تكشف الأخطاء الداخلية للعملاء في الإنتاج
if (process.env.NODE_ENV === 'production') {
// التحقق مما إذا كان نوع خطأ معروف
if (error.extensions?.code) {
return error;
}
// إخفاء الأخطاء غير المتوقعة
return {
message: 'Internal server error',
extensions: {
code: 'INTERNAL_SERVER_ERROR'
}
};
}
// في التطوير، إرجاع الخطأ الكامل
return error;
}
});
تحذير: لا تكشف أبدًا أخطاء قاعدة البيانات أو تتبعات المكدس أو تفاصيل التنفيذ الداخلية للعملاء في الإنتاج. أخفِ دائمًا الأخطاء غير المتوقعة.
البيانات الجزئية مع الأخطاء
يسمح GraphQL بإرجاع بيانات جزئية عند فشل بعض الحقول:
const resolvers = {
Query: {
dashboard: async (_, __, context) => {
if (!context.user) {
throw new AuthenticationError('Login required');
}
return {}; // إرجاع كائن فارغ، الحقول تحل بشكل مستقل
}
},
Dashboard: {
profile: async (parent, _, context) => {
try {
return await User.findById(context.user.id);
} catch (error) {
console.error('Profile fetch failed:', error);
return null; // فشل جزئي - الحقول الأخرى لا تزال تحل
}
},
posts: async (parent, _, context) => {
try {
return await Post.find({ userId: context.user.id });
} catch (error) {
console.error('Posts fetch failed:', error);
throw new Error('Failed to load posts');
}
},
stats: async (parent, _, context) => {
return await Stats.getForUser(context.user.id);
}
}
};
// نتيجة الاستعلام مع بيانات جزئية:
// {
// "data": {
// "dashboard": {
// "profile": null,
// "posts": null,
// "stats": { "views": 1250, "followers": 42 }
// }
// },
// "errors": [
// {
// "message": "Failed to load posts",
// "path": ["dashboard", "posts"]
// }
// ]
// }
معالجة الأخطاء على مستوى الحقل
const resolvers = {
User: {
email: (parent, _, context) => {
// إظهار البريد الإلكتروني فقط للمستخدم نفسه أو المسؤولين
if (context.user?.id === parent.id || context.user?.isAdmin) {
return parent.email;
}
throw new ForbiddenError('Email is private');
},
privateData: async (parent, _, context) => {
if (context.user?.id !== parent.id) {
throw new ForbiddenError('Cannot access private data');
}
try {
return await fetchPrivateData(parent.id);
} catch (error) {
console.error('Private data fetch failed:', error);
throw new Error('Failed to load private data');
}
}
}
};
نصيحة: استخدم أكواد الأخطاء في حقل
extensions للسماح للعملاء بمعالجة أنواع مختلفة من الأخطاء برمجيًا دون تحليل رسائل الخطأ.
تنسيق أخطاء Apollo Server
const { ApolloServer } = require('apollo-server');
const { v4: uuidv4 } = require('uuid');
const server = new ApolloServer({
typeDefs,
resolvers,
formatError: (error) => {
// إنشاء معرف خطأ للتتبع
const errorId = uuidv4();
// تسجيل محسّن
console.error('GraphQL Error:', {
errorId,
message: error.message,
code: error.extensions?.code,
path: error.path,
locations: error.locations,
originalError: error.originalError
});
// إضافة معرف الخطأ إلى الاستجابة
return {
...error,
extensions: {
...error.extensions,
errorId,
timestamp: new Date().toISOString()
}
};
},
context: async ({ req }) => {
// يمكن أن يطرح السياق أخطاء أيضًا
const token = req.headers.authorization?.replace('Bearer ', '');
if (token) {
try {
const user = await verifyToken(token);
return { user };
} catch (error) {
throw new AuthenticationError('Invalid token');
}
}
return {};
}
});
تمرين:
- أنشئ فئات أخطاء مخصصة لـ
RateLimitErrorوConflictError - نفذ محللًا يطرح أنواع أخطاء مختلفة بناءً على منطق الأعمال
- أضف تسجيل الأخطاء مع معرفات أخطاء يمكن للعملاء الرجوع إليها
- أنشئ دالة
formatErrorتخفي تتبعات المكدس في الإنتاج - اختبر تسليم البيانات الجزئية بجعل حقل واحد يطرح خطأ بينما تنجح الحقول الأخرى