معالجة الأخطاء

التقط الاستثناءات من فئات أخطاء listingsapi-js، واقرأ رموز أخطاء المنصة، وأنشئ آلية لتأخير إعادة المحاولة تراعي retryAfter.

كل خطأ تطرحه حزمة SDK مشتق من ListingsAPIError. تؤدي حالات فشل API إلى طرح APIError أو إحدى فئاته الفرعية، سواء أبلغت عنها المنصة برمز حالة HTTP خارج النطاق 2xx أو ضمن جسم استجابة HTTP 200. تؤدي حالات فشل الشبكة (أخطاء DNS ورفض الاتصالات وانتهاء المهلة) إلى طرح APIConnectionError. لا تحتاج أبدًا إلى فحص علامات success أو الحمولات الخام: اكتمال promise بنجاح يعني نجاح الاستدعاء.

التسلسل الهرمي للأخطاء

Code
ListingsAPIError                 base class for everything the SDK throws├── APIError                     any failure reported by the API│   ├── AuthenticationError      401, or invalid-token error payloads│   ├── PermissionDeniedError    403, or permission error payloads│   ├── NotFoundError            404, resource not found│   ├── ValidationError          400/422, mutations reporting success=false│   │                            or errors[], and client-side payload checks│   ├── RateLimitError           429, carries retryAfter│   └── InternalServerError      5xx, retry a read; read back a write first└── APIConnectionError           no response arrived: outcome unknown

تُصدَّر جميع الفئات من جذر الحزمة:

TypeScript
import {ListingsAPIError,APIError,AuthenticationError,PermissionDeniedError,NotFoundError,ValidationError,RateLimitError,InternalServerError,APIConnectionError,} from 'listingsapi-js';

خصائص APIError

الخاصيةالنوعالوصف
statusCodenumber | nullرمز حالة HTTP للاستجابة الفاشلة: 401 أو 429 أو 500، أو 200 عندما يرد الخطأ ضمن جسم استجابة 200. تكون القيمة null في حالات فشل التحقق من صحة البيانات من جانب العميل.
codestring | nullأول رمز خطأ من المنصة، e.g. SY10005، عندما ترسل API رمزًا.
errorsApiErrorEntry[]جميع عناصر الأخطاء، موحَّدة البنية وفقًا لـ { code, message, context }.
responseBodystring | nullجسم الاستجابة الخام كسلسلة نصية.
retryAfternumber | nullخاص بـ RateLimitError فقط. عدد الثواني المطلوب انتظارها قبل إعادة المحاولة، ويُستخرج من الترويسة Retry-After، أو تكون القيمة null عند غياب الترويسة.
messagestringملخص مقروء للمستخدم تُلحَق به عناصر الأخطاء (أو الجسم الخام).
namestringاسم الفئة الفعلية، e.g. "RateLimitError".
TypeScript
import { ListingsAPI, APIError, APIConnectionError } from 'listingsapi-js'; const client = new ListingsAPI(); try {await client.fetchAllLocations({ first: 10 });} catch (error) {if (error instanceof APIError) {  console.error(error.statusCode);   // 401, 429, 500, or 200 for payload errors  console.error(error.code);         // first platform code, e.g. 'SY10005'  console.error(error.errors);       // [{ code, message, context }, ...]  console.error(error.responseBody); // raw body text} else if (error instanceof APIConnectionError) {  // No response arrived. A read is safe to retry; for a write, read the  // record back before repeating it.  console.error('No response from the API:', error.message);}}

التقاط الأخطاء حسب النوع

حالات فشل المصادقة

TypeScript
import { AuthenticationError } from 'listingsapi-js'; try {await client.fetchAllLocations({ first: 1 });} catch (error) {if (error instanceof AuthenticationError) {  console.error('Invalid API key. Rotate it under API Keys in the dashboard.');}}

الموارد غير الموجودة

TypeScript
import { NotFoundError } from 'listingsapi-js'; try {await client.fetchPremiumListings(99999999);} catch (error) {if (error instanceof NotFoundError) {  console.error('Location not found.');}}

حدود معدل الطلبات

يحتوي RateLimitError.retryAfter على قيمة Retry-After الواردة من الخادم بالثواني، أو null عند غياب الترويسة:

TypeScript
import { RateLimitError } from 'listingsapi-js'; try {await client.fetchAllLocations({ first: 100 });} catch (error) {if (error instanceof RateLimitError) {  const waitSeconds = error.retryAfter ?? 5;  console.error('Rate limited, retry in ' + waitSeconds + 's');}}

حالات فشل التحقق من صحة البيانات

تتضمن حالات فشل التحقق من صحة البيانات من جانب الخادم عناصر الأخطاء الواردة من المنصة:

TypeScript
import { ValidationError } from 'listingsapi-js'; try {await client.updateLocation({ id: 16808, phone: 'not-a-phone-number' });} catch (error) {if (error instanceof ValidationError) {  for (const entry of error.errors) {    console.error((entry.code ?? 'unknown') + ': ' + entry.message);  }}}

الأخطاء ضمن استجابات HTTP 200

تُبلغ المنصة عن بعض حالات الفشل برمز HTTP 200 وحمولة خطأ ضمن جسم الاستجابة. تفحص حزمة SDK كل استجابة وتطرح أخطاء في هذه الحالات أيضًا، لذا لا تعني الحالة 200 أبدًا "افحص الجسم بنفسك":

علامة الخطأ في الحمولةيُطرح على هيئة
errors[] في المستوى الأعلى مع الرمز SY90005 أو SY90001AuthenticationError
errors[] في المستوى الأعلى مع الرمز SY90003PermissionDeniedError
errors[] في المستوى الأعلى مع الرمز RATE_LIMITEDRateLimitError
أي errors[] آخر في المستوى الأعلىAPIError
نتيجة عملية كتابة تتضمن errors[] أو success: falseValidationError

في عمليات الكتابة (الدالتان المساعدتان POST وDELETE)، تفحص حزمة SDK كل كائن نتيجة في الاستجابة؛ وإذا أشار أي منها إلى success: false أو تضمّن errors[]، فإنها تطرح ValidationError مع تعيين statusCode إلى 200.

TypeScript
import { ValidationError } from 'listingsapi-js'; try {const result = await client.archiveLocations([16808]);console.log('Archived:', result);} catch (error) {if (error instanceof ValidationError) {  // The API answered 200 but rejected the mutation  console.error(error.statusCode); // 200  console.error(error.message);}}

التحقق من صحة البيانات من جانب العميل في الدوال المساعدة للمنشورات

تتحقق الدوال المساعدة للمنشورات (createAnnouncement وcreateEvent وcreateOffer وbulkPublish) من صحة حمولتها محليًا وتطرح ValidationError قبل إجراء أي استدعاء عبر الشبكة:

  • يجب ألا يكون name فارغًا أو مكوَّنًا من مسافات بيضاء فقط، ويجب ألا يكون locationIds فارغًا.
  • يجب أن يكون sites مجموعة فرعية غير فارغة من المجموعة التي تضم GOOGLE وFACEBOOK.
  • message مطلوب؛ وعندما يكون خريطة بقيم منفصلة لكل موقع، يجب أن يكون لكل موقع مستهدف عنصر غير فارغ ولا يقتصر على مسافات بيضاء.
  • يجب توفير ctaType وctaUrl معًا، ويجب أن تكون قيمة ctaType إحدى القيم BOOK أو ORDER أو SHOP أو LEARN_MORE أو SIGN_UP أو GET_OFFER.
  • يتطلب createEvent وcreateOffer أيضًا توفير title.
  • يجب أن تكون قيمة postType (في bulkPublish) إحدى القيم ANNOUNCEMENT أو EVENT أو OFFER أو COVID19 أو PRODUCT.

تتضمن هذه الأخطاء المحلية statusCode: null، وهو ما يتيح تمييزها عن الرفض من جانب الخادم. ترسل الطريقة منخفضة المستوى createPost(body) جسمها الخام دون إجراء هذه الفحوص.

TypeScript
import { ListingsAPI, ValidationError } from 'listingsapi-js'; const client = new ListingsAPI(); try {await client.createAnnouncement({  name: 'Grand opening',  locationIds: [16808],  message: 'Acme Dental is now open in Midtown!',  ctaType: 'BOOK', // ctaUrl missing: throws before any request is sent});} catch (error) {if (error instanceof ValidationError && error.statusCode === null) {  console.error(error.message);  // Invalid post: ctaType and ctaUrl must be provided together}}

إعادة المحاولة المدمجة

يعيد العميل نفسه المحاولة عند تلقي 429 واستجابات 5xx (مع الالتزام بالترويسة Retry-After) وعند فشل الشبكة، وذلك مرتين افتراضيًا. لا تصل الأخطاء إلى شفرتك إلا بعد استنفاد تلك المحاولات. راجع الإعدادات للاطلاع على الخيارين maxRetries وtimeout.

مثال لإعادة المحاولة بفترات انتظار مع مراعاة retryAfter

للمهام طويلة التشغيل التي ينبغي أن تستمر بعد استنفاد محاولات العميل المدمجة، ضع الاستدعاءات داخل حلقة إعادة محاولة بفترات انتظار تعطي الأولوية لتوجيه الخادم retryAfter، وتستخدم فترات انتظار متزايدة أُسّيًا عند غيابه:

backoff.ts
import {ListingsAPI,RateLimitError,InternalServerError,APIConnectionError,} from 'listingsapi-js'; const client = new ListingsAPI(); async function withBackoff<T>(fn: () => Promise<T>,maxAttempts = 5,): Promise<T> {for (let attempt = 1; attempt <= maxAttempts; attempt++) {  try {    return await fn();  } catch (err) {    const retryable =      err instanceof RateLimitError ||      err instanceof InternalServerError ||      err instanceof APIConnectionError;    if (!retryable || attempt === maxAttempts) throw err;     let delayMs = 500 * 2 ** (attempt - 1);    if (err instanceof RateLimitError && err.retryAfter !== null) {      delayMs = err.retryAfter * 1000;    }    await new Promise((resolve) => setTimeout(resolve, delayMs));  }}throw new Error('unreachable');} const page = await withBackoff(() => client.fetchAllLocations({ first: 25 }));

لا تُعاد المحاولة عمدًا عند حدوث NotFoundError أو ValidationError أو AuthenticationError أو PermissionDeniedError: لا يمكن أن تنجح هذه الطلبات عند تكرارها دون تغيير المدخلات أو المفتاح.

حالات فشل الشبكة وانتهاء المهلة

يؤدي أي أمر يمنع وصول الاستجابة (فشل DNS أو رفض الاتصال أو انتهاء المهلة المحددة لكل طلب) إلى طرح APIConnectionError. لا تمرر حزمة SDK أبدًا TypeError أو AbortError الأصليين الصادرين من fetch إلى الشفرة المستدعية.

APIConnectionError لا يعني أن الطلب لم يصل إلى الخادم. يتوقف الطلب بالفعل قبل وصوله إلى API عند فشل DNS أو رفض الاتصال، لكن انتهاء المهلة يندرج ضمن الفئة نفسها، وقد يكون الطلب قد وصل واكتمل بعد أن توقف العميل عن الانتظار. تعامل مع الحالة على أن النتيجة غير معروفة: يمكنك إعادة محاولة القراءة دون قيود، أما في عملية الكتابة فاقرأ السجل مجددًا قبل تكرارها، لأن API لا تكتشف عمليات الكتابة المكررة.

تُطبَّق المهلة داخليًا باستخدام AbortSignal.timeout في كل محاولة، وتبلغ افتراضيًا 240000 مللي ثانية، ويمكن ضبطها عبر خيار المُنشئ timeout. تُعاد المحاولة التي انتهت مهلتها مثل أي حالة فشل اتصال أخرى؛ ولا يُطرح إلا الخطأ الناتج عن الفشل النهائي:

TypeScript
import { ListingsAPI, APIConnectionError } from 'listingsapi-js'; const client = new ListingsAPI({ timeout: 15000 }); try {await client.fetchAllLocations({ first: 10 });} catch (error) {if (error instanceof APIConnectionError) {  console.error('Network failure or timeout:', error.message);}}