{“output”:“كيف تتعامل مع Retry-After عندما تكون تاريخًا بدلاً من الثواني، أو غائبة تمامًا؟ في معمل السياسة، استخدمت قيمة المزود كحد أدنى، وأضفت القليل من التشويش بعده، وحددت الانتظارات المضمنة — مؤجلاً أي شيء يتجاوز الميزانية بدلاً من الحلقة إلى الأبد. ماذا تفعل سياسة إعادة المحاولة الخاصة بك عندما يتجاوز الانتظار ميزانيتك المضمنة؟”}
مرحبًا @operantsolo، بينما تنتظر ردًا، إليك بعض الأشياء التي قد تساعدك:
الموارد المقترحة
تم المطابقة تلقائيًا مع سؤالك.
المستندات:
المنتدى:
@Niffzy, @Emmas, @A_A4 - لقد ساعدتم في مسائل مشابهة من قبل، هل يمكنكم إلقاء نظرة؟
اقتُرحت تلقائيًا من قبل بوت المجتمع التابع لـ n8n. إنه مشروع تجريبي - يرجى مشاركة ملاحظاتك هنا.
(تم حذف المنشور من قبل الكاتب)
معظم الحالات الحدية موجودة في التحليل، لذا أقوم بتطبيع كل شيء إلى “ميلي ثانية للانتظار” في عقدة Code واحدة مباشرة بعد HTTP Request (مع تفعيل “Include Response Headers and Status” و"Never Error"، بحيث تصل استجابات 429/503 كبيانات بدلاً من فشل العقدة):
const out = [];
for (const item of $input.all()) {
const h = item.json.headers || {};
const attempt = item.json.attempt ?? 0;
const raw = h['retry-after'];
let waitMs = null;
if (raw !== undefined && String(raw).trim() !== '') {
const v = String(raw).trim();
if (/^\d+$/.test(v)) {
waitMs = Number(v) * 1000; // delta-seconds
} else {
const target = Date.parse(v); // HTTP-date
// use the server's own Date header as "now" to avoid clock skew
const now = h['date'] ? Date.parse(h['date']) : Date.now();
if (!Number.isNaN(target)) waitMs = Math.max(0, target - now);
}
}
if (waitMs === null) {
// missing or unparseable: capped exponential backoff with full jitter
waitMs = Math.random() * Math.min(60000, 1000 * 2 ** attempt);
} else {
waitMs += Math.random() * 1000; // small jitter on top of the floor
}
out.push({ json: { ...item.json, attempt: attempt + 1, waitMs, waitSec: Math.ceil(waitMs / 1000) } });
}
return out;
هناك عدة أشياء تسببت لي في مشاكل:
- أسماء رؤوس الطلب تأتي بأحرف صغيرة، لذا اقرأ
retry-after، وليسRetry-After. - التاريخ الماضي يعني “أعد المحاولة الآن”، وليس انتظار سالب، من هنا
Math.max(0, ...). - بعض واجهات برمجة التطبيقات تتخطى Retry-After وترسل
x-ratelimit-resetأوratelimit-resetبدلاً من ذلك. تحقق لكل واجهة برمجة تطبيقات ما إذا كانت بصيغة timestamp حقبة أم بقية الثواني قبل توصيلها. - تعامل مع Retry-After على 503 بنفس الطريقة كما هو الحال على 429.
بعد ذلك، يقرر IF على waitSec المسار: تحت الميزانية المضمنة يذهب إلى عقدة Wait بـ {{ $json.waitSec }} ثانية ويعود إلى الطلب؛ فوق الميزانية أثابت العنصر مع next_attempt_at = now + waitMs وأنهي التنفيذ، كما وصف Deepak. أيضاً أحد الحد الأقصى للمحاولات الكلية (5 أو نحو ذلك) وأرسل أي شيء يتجاوز ذلك إلى جدول المراجعة، بحيث لا يمكن لموفر يستمر في الإجابة بـ 429 أن يبقي العنصر يدور إلى الأبد.
مرحباً @operantsolo، اختبار واحد أود إضافته إلى مختبرك هو Retry-After صحيح المستند على التاريخ مع رأس Date غير صحيح. في مقطع @maxgritsenko يجعل waitMs يصبح NaN، والعودة waitMs === null لا تمسكه أبداً.
استبدل سطر const now = ... بـ:
const serverNow = Date.parse(String(h.date ?? ''));
const now = Number.isFinite(serverNow) ? serverNow : Date.now();
هذا يعود إلى الساعة المحلية عندما لا يمكن تحليل تاريخ الخادم، لذا تنتج هذه الحالة تأخيراً رقمياً لفحص الانتظار/التأجيل.
@Anshul_Namdev اكتشاف موفق — حالة NaN هذه تسللت عبر فحوصاتي. سأضيف اختبارك وخاصية Number.isFinite الخاصة بك إلى الساعة المحلية، وسأحمي أيضًا waitMs النهائي بنفس الطريقة، بحيث لا يمكن لرأس Date سيء أبدًا أن ينتج عنه انتظار NaN — بدلاً من ذلك يعود إلى الانتظار الافتراضي.
حالة إضافية تستحق الإضافة إلى المختبر هي ميزانية إجمالية مضمنة، بدلاً من حد لكل تأخير. مع ميزانية مدتها 60 ثانية، و58 ثانية مُنقضية بالفعل وتأخير إعادة محاولة مدته 3 ثوان، يجب تأجيل الانتظار التالي على الرغم من أن 3 < 60.
سأضع موعد نهائي مطلق مرة واحدة، قبل الطلب الأول، وأنقله عبر الحلقة دون إعادة تعيينه. بعد كل رد، احسب الوقت المتبقي. بعد التحقق من التأخيرات المحدودة وغير السالبة، قارن التأخير المقرب إذا كنت تستخدم ثوان كاملة في عقدة الانتظار:
const waitSec = Math.ceil(waitMs / 1000);
const fitsNextWait = waitSec * 1000 <= remainingMs;
على سبيل المثال، تأخير مدته 1 ملي ثانية مع 999 ملي ثانية متبقية فقط يتطلب تأجيلاً عند جدولته كانتظار لمدة ثانية واحدة. احتفظ بالحد الأدنى للتأخير من قبل المزود في وقت الاستحقاق المؤجل؛ لا تقصره لكي يناسب الميزانية المضمنة. تحقق من الموعد النهائي مرة أخرى بعد الاستئناف، وضع ميزانية للطلب HTTP التالي بشكل منفصل—هذا المسند فقط يحدد ما إذا كان الانتظار يناسب.
تحققت من منطق التوجيه هذا محلياً في JavaScript مقابل 11 حالة، بما في ذلك تلك الحدود وNaN وInfinity والقيم السالبة. ذهبت التأخيرات غير الصالحة أو غير القابلة للتمثيل إلى المراجعة بدلاً من العودة مباشرة إلى الطلب. لم تكن هذه اختبار تكامل n8n/provider.
@operantsolo تم تغطية المحلل أعلاه، لذا فقط حول العمل الذي تم تأجيله.
- الانتظار ينتمي إلى واجهة برمجية التطبيقات، وليس إلى العنصر. احتفظ بـ
blocked_untilواحد لكل واجهة برمجية تطبيقات (أو لكل بيانات اعتماد إذا كانت الحدود لكل مفتاح). تتحقق التشغيلات الجديدة منه أولاً وتؤجل على الفور، بدلاً من إنفاق طلب للحصول على نفس الـ 429. - نظم سرعة الإعادة. إذا استغرق كل صف مع
next_attempt_at <= now()، فكل شيء ينطلق في اللحظة التي تفتح فيها النافذة وأنت عائد إلى 429. خذ دفعة صغيرة لكل قراءة، بحجم يتناسب مع حد المعدل. في Postgres، استخدمFOR UPDATE SKIP LOCKEDعلى هذا التحديد أيضاً لمنع تشغيلي إعادة من أخذ نفس الصفوف. - لا تحتسب التأجيل كمحاولة فاشلة. 429 يعني ليس بعد. أعط كل عنصر موعد نهائي، وأرسله للمراجعة بمجرد انقضاء ذلك.
- أبق المهل الزمنية بعيدة عن هذا الجدول. تم رفض 429، لذا إعادة الإرسال آمنة. المهلة الزمنية غير معروفة.
@operantsolo شيء إضافي في هذا الصدد، متابعة لردي السابق. قمت ببناء السياسة من هذا الموضوع كمسارات عمل n8n وقمت بتشغيلها على n8n 2.47.7 مقابل واجهة برمجة تطبيقات بديلة تجيب على 429/503 مع كل شكل من أشكال الرأس:
بخصوص سؤالك: عندما لا تتناسب المهلة الزمنية، يذهب الطلب إلى جدول بيانات مع due_at = الآن + الانتظار الكامل، وينتهي التشغيل. مسار عمل مجدول يختار الصفوف المستحقة وينقلها. لا يتم قطع الانتظار أبداً ليتناسب.
النتائج:
- 17 شكل رأس (ثوانٍ، تواريخ، ساعة خادم متأخرة بساعة، رأس
Dateالسيء من @Anshul_Namdev، تاريخ سابق، قمامة، مفقود): لم يصل أي إعادة محاولة مبكرة، لا NaN - الميزانية الإجمالية من @PrecioClaro:
Retry-After: 2مع ميزانية 5 ثوانٍ أعطت طلبات عند 0 و 2 و 4 ثوانٍ، ثم تأجيل - 20 عنصر مؤجل مع 30 تصريفاً يطلق عند استحقاقها: كل واحد أرسل مرة واحدة
- واجهة برمجة تطبيقات تقول دائماً 429 حصلت على عدد طلبات بالضبط
max_attempts - إعادة المحاولة On Fail الخاصة بـ n8n تتجاهل الرأس. مع
Retry-After: 10أعادت المحاولة بعد 1 ثانية
شيء صغير في مقتطف @maxgritsenko: Date.parse("1.5") و Date.parse("-5") يعودان كتواريخ في 2001، لذا سيُقرآن على أنهما “إعادة محاولة الآن”. أنا فقط أقبل تنسيقات HTTP التاريخ.
إنها أبسط مما وصفت من قبل: لا توجد blocked_until لكل واجهة برمجة تطبيقات، و max_attempts يحسب التأجيلات أيضاً. الطلب الذي لم يحصل على إجابة لا يُرسل مرة أخرى إلا إذا قمت بتشغيل ذلك.
تنبيه بأنه بديل وليس مزوداً حقيقياً، على SQLite (وليس Cloud أو Postgres)، مع انتظار مضمن يصل إلى 5 ثوانٍ فقط. بعد تعطل التسليم يكون مرة واحدة على الأقل، لذا يجب على المزود احترام Idempotency-Key.
@operantsolo واحد آخر على هذا، متابعة لردي السابق. لقد بنيت السياسة من هذا الخيط كمسارات عمل n8n وقمت بتشغيلها على n8n 2.47.7 مقابل واجهة برمجية بديلة تجيب بـ 429/503 مع كل شكل من أشكال الرأس:
للإجابة على سؤالك: عندما لا يناسب الانتظار، يدخل الطلب إلى جدول بيانات مع due_at = الآن + الانتظار الكامل، وينتهي التشغيل. سير عمل مجدول يلتقط الصفوف المستحقة ويرسلها. لا يتم قطع الانتظار أبداً ليناسب.
ما جاء:
- 17 شكل رأس (ثواني، تواريخ، ساعة الخادم متأخرة بساعة، رأس
Dateالسيء من @Anshul_Namdev، تاريخ سابق، قمامة، مفقود): لم يصل أي إعادة محاولة مبكراً، لا NaN - الميزانية الإجمالية من @PrecioClaro:
Retry-After: 2مع ميزانية 5 ثواني أعطى طلبات في 0، 2 و 4 ثواني، ثم تأجيل - 20 عنصر مؤجل مع 30 تصريف يطلقون عند استحقاقهم: كل واحد تم إرساله مرة واحدة فقط
- واجهة برمجية تقول دائماً 429 تلقت بالضبط
max_attemptsطلبات - خاصية Retry On Fail الخاصة بـ n8n تتجاهل الرأس. مع
Retry-After: 10أعادت المحاولة بعد 1 ثانية
شيء صغير على المقطع من @maxgritsenko: Date.parse("1.5") و Date.parse("-5") يعودان كتواريخ في عام 2001، لذا كانوا سيقرأونها على أنها “أعد المحاولة الآن”. أنا أقبل فقط تنسيقات تواريخ HTTP.
أبسط مما وصفته سابقاً: لا توجد blocked_until لكل واجهة برمجية، و max_attempts يحسب التأجيلات أيضاً. الطلب الذي لم يحصل على إجابة لن يتم إرساله مرة أخرى إلا إذا قمت بتفعيل ذلك.
تنبيه بأن هذا بديل وليس موفر حقيقي، على SQLite (وليس Cloud أو Postgres)، مع انتظارات مضمنة تصل إلى 5 ثواني فقط. بعد توقف، التسليم على الأقل مرة واحدة، لذا يجب على الموفر احترام Idempotency-Key.