الاستجابة المتماثلة في واجهات برمجة التطبيقات — لماذا يتم تشغيل طلب POST مرتين وطريقة إصلاحه

تحديث في

إذا تم فرض دفعة مرتين، أو تمت إنشاء طلب مكرر، أو تم إنشاء مستخدم ثلاث مرات — فإن هذه الأعراض تدل على وجود عقد واجب تطبيقي مفقود. إليك ما يعنيه المفهوم المُسمى "الإثبات المكرر"، ولماذا يُؤثر التحديد POST عليه، وكيف يُعالج هذا المفهوم باستخدام مفاتيح الإثبات المكرر.

أي دفع مكرر، طلب مكرر، أو مستخدم مُنشأ ثلاث مرات — هذه أعراض نفس العقد المفقود في واجهة برمجة التطبيقات. إليك ما يعنيه التكرار، لماذا يُنهار POST، وكيف يُعالج التكرار باستخدام مفاتيح التكرار.
إعلان · حذف؟

انقر المستخدم على "دفع الآن". بدأ الدوران. انتهاء الاتصال بالشبكة. بدأ محاولة التكرار. تم تمرير الطلب. ثم اكتمل الطلب الأصلي أيضًا من جانب الخادم — وتم تمرير الطلب مرة أخرى. $200 تم سحبها من حساب العميل، وطلب دعم ينتظرك في الصباح.

هذا ليس حالة نادرة. هذا ما يحدث عندما لا تفكر في التكرار قبل الحاجة إليه. دعنا نصل إلى حل لهذا.

ما يعنيه التكرار الفعلي

الكلمة تأتي من الرياضيات. إذا تم تطبيق عملية متعددة المرات، فإن النتيجة تكون نفسها كما لو أنك قمت بتطبيقها مرة واحدة. f(f(x)) = f(x).

في مصطلحات واجهة برمجة التطبيقات: يجب أن تُرسل نفس الطلب إلى نفس النقطة بذات الهدف N مرات، ويجب أن يبقى النظام في نفس الحالة كما لو أنك قمت بتنفيذها مرة واحدة. يمكن أن تكون النتيجة محفوظة في الذاكرة، ولكن التأثيرات الجانبية — مثل الكتابات في قاعدة البيانات، أو التحويلات، أو الإيميلات — يجب أن تحدث مرة واحدة فقط.

هذا ضمان حول ما يفعله خادمك إلا أنّها تُوسع النطاق لتشمل المواقع الفرعية. احذف السمة إذا أردت أن تكون الأكواد محدودة بالموقع.، وليس فقط ما يُرجعه.

طرق HTTP: من يُعتبر آمنًا ومن لا يُعتبر

يُحدد معيار HTTP معينًا من الطرق كمُتكرر بتعريف. إليك التحليل العملي:

طريقةمُتكرر؟لماذا
GET✅ نعمقراءة فقط. لا توجد تأثيرات جانبية بتعريف.
HEAD✅ نعمنفس GET، لا يُعاد إرسال جسم.
PUT✅ نعم"أعد هذا المورد إلى الحالة المحددة." عند التسجيل مرتين، يُنتج نفس النتيجة.
DELETE✅ نعميُزال المورد بعد الطلب الأول. عند الطلب التالي، لا يوجد شيء لحذفه. (قد تختلف الرمز — 204 مقابل 404 — لكن الحالة المُستخدمة لا تتغير).
POST❌ لا"معالجة هذا المحتوى". ما يعنيه هذا يعتمد على الخادم. عادةً ما تُنتج الطلبين موارد مُختلفة أو تُحفّز تأثيرات جانبية مُختلفة.
PATCH⚠️ يعتمدتحديث مُستند ("increment count by 1") ليس مُتكررًا. تحديث مُطلق ("set count to 5") هو. يُحدد هذا من قبل التنفيذ.

الإشكالية هي أن معظم العمليات التجارية الحقيقية — مثل دفع مبلغ، إنشاء طلب، إرسال إشعار — تُمثل بشكل طبيعي بطلب POST. ويوفر POST تأكيدات مُتكررة من البداية.

السلسلة التي تُحصل بها

إليك النمط الكلاسيكي للإخفاق، خطوة بخطوة:

Client                    Network                   Server
  |
  |--- POST /payments ------>|                         |
  |                          |--- (delivered) -------->|
  |                          |                  processing...
  |                          |                  card charged ✓
  |<-- (connection drops) ---|                  response queued
  |
  |  [retry logic kicks in]
  |
  |--- POST /payments ------>|                         |
  |                          |--- (delivered) -------->|
  |                          |                  processing...
  |                          |                  card charged ✓ (again)
  |<------- 200 OK ----------|<------------------------|
  |
  [client sees: one charge. server did: two charges]

لم يُرى العميل أي رد فعل. من وجهة نظره، فشل الطلب. من وجهة نظر الخادم، تم تنفيذ الطلب مرتين. هذا نمط شائع في أنظمة التوزيع — يُفترض أن العميل والخادم اختلفوا في ما حدث.

هذا ليس فقط مُعالجات الدفع. هذا يشمل إنشاء الطلبات، تسجيل المستخدمين، إرسال الإيميلات، تحديد المخزون — أي شيء يُؤثر عليه "الإطلاق مرتين" بشكل حقيقي.

مفاتيح التكرار: نمط ستريب

أصبحت طريقة ستريب مُحببة، وتم استخدامها الآن في معظم واجهات الدفع. يُولّد العميل مفتاحًا فريدًا قبل إرسال الطلب ويُرفق به كرأس. يستخدم الخادم هذا المفتاح لمنع التكرار.

من جانب العميل:

// Generate once, before any retry loop
const idempotencyKey = crypto.randomUUID();

async function chargeWithRetry(amount, retries = 3) {
  for (let attempt = 0; attempt < retries; attempt++) {
    try {
      const response = await fetch('/api/payments', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Idempotency-Key': idempotencyKey, // same key on every retry
        },
        body: JSON.stringify({ amount, currency: 'usd' }),
      });

      if (response.ok) return await response.json();

      // Don't retry on client errors (4xx)
      if (response.status < 500) throw new Error(`Client error: ${response.status}`);

    } catch (err) {
      if (attempt === retries - 1) throw err;
      await sleep(Math.pow(2, attempt) * 1000); // exponential backoff
    }
  }
}

يجب أن يُولّد المفتاح 🔒 فحوصات الأمان: في أي دورة إعادة التسجيل — وهذا هو الهدف. إذا قمت بولّد UUID جديد في كل محاولة، فقد قمت بتعطيل الميكانيزم تمامًا.

يُستخدم UUID v4 بشكل جيد هنا. إذا كنت بحاجة إلى إنتاجه بسرعة أثناء الاختبار، IO Tools' مولد UUID يُمكنك إنتاج مجموعة منه دون الحاجة إلى تحميل مكتبة.

من جانب الخادم: احفظ الرد، وارجعه عند التكرار

مهمة الخادم مفهومة ببساطة: تحقق مما إذا كان هذا المفتاح قد تم رؤيته من قبل؛ إذا نعم، أعد الرد المحفوظ؛ إذا لا، اعمل عليه واحفظه.

// Express + Redis
app.post('/api/payments', async (req, res) => {
  const idempotencyKey = req.headers['idempotency-key'];

  if (idempotencyKey) {
    const cached = await redis.get(`idem:${idempotencyKey}`);
    if (cached) {
      const { status, body } = JSON.parse(cached);
      return res.status(status).json(body);
    }
  }

  const result = await paymentProcessor.charge(req.body);
  const responseBody = { id: result.id, status: result.status };

  if (idempotencyKey) {
    // 24-hour TTL — same window Stripe uses
    await redis.setex(
      `idem:${idempotencyKey}`,
      86400,
      JSON.stringify({ status: 200, body: responseBody })
    );
  }

  res.json(responseBody);
});

أبعاد مهمة:

  • تُعالج الفشل، وليس فقط النجاح. إذا فشل الدفع (تم رفض البطاقة)، احفظ هذا الرد الفاشل أيضًا. وإلا فإن إعادة المحاولة ستجري الدفع مرة أخرى حتى لو كان قد عادت خطأ — وهذا ما تسعى لتجنبه.
  • تحقق من توازن الجسم. تُرجع ستريب 422 إذا تم استخدام نفس المفتاح مع معلمات طلب مختلفة. هذا يُكتشف الأخطاء التي تُستخدم فيها مفتاح مُكرر في عمليات مختلفة.
  • تُعالج التكرارات المُتوازية. إذا وصلت طلبات متماثلة في نفس المفتاح بشكل متوازي، يجب التنسيق — اعمل على واحد وارجع 409 على الآخر، أو استخدم مفتاح توزيعي. يُستخدم نمط SETNX في هذا السياق.
  • ضع فترة انتقال منطقية. بعد انتهاء الفترة، يمكن إعادة استخدام المفاتيح ويعتبر الطلب المتماثل جديدًا. لا تُحفظه إلى الأبد — سينمو مخزنك.

أخطاء من جانب العميل تُرسل POST مرتين دون إعادة المحاولة

تُحمي مفاتيح التكرار من إعادة التسجيل في الطبقات الشبكية. لا تساعد عندما يُرسل العميل طلبات منفصلة ومستقلة. أسباب شائعة:

  • النقر المزدوج على زر الإرسال. يُنقر المستخدم، لا يرى أي شيء يحدث، ثم ينقر مرة أخرى. اوقف الزر فورًا عند النقر الأول — وليس بعد وصول الرد. الفجوة بين النقر والرد هي اللحظة التي يُنقر فيها النقر الثاني.
  • إعادة التسجيل في React StrictMode. يُنفذ React 18 Strict Mode مرتين في التطوير للكشف عن الأخطاء. إذا كنت تُرسل POST في useEffect بدون تنظيف، سترى طلبات مكررة في التطوير. لا يحدث في الإنتاج، لكنه قد يُخفف من المشكلة الحقيقية.
  • إعادة التسجيل للنماذج في المتصفح. إرسال → توجه → عودة → تقدم. بعض المتصفحات تُعرض "إعادة التسجيل؟"، وبعضها يفعل ذلك تلقائيًا. يُزيل نمط POST-REDIRECT-GET (يُرجع توجيهًا بعد POST) هذا تمامًا.
  • الظروف المتنافسة في معالجات الأحداث. نقرة وضغط مفتاح سريع يُحفّز معالج التسجيل قبل وصول الرد الأول ويُوقف النموذج.
  • إعادة التسجيل في تطبيقات الهاتف المحمول. يمكن أن تُعيد تطبيقات iOS وAndroid التسجيل في الخلفية أو في الطبقات الشبكية. اولّد مفاتيح التكرار في لحظة التسجيل، واحتفظ بها محليًا إذا لزم، واحذفها فقط بعد التأكيد على النجاح.

خيار بديل يستحق التفكير فيه: PUT إلى عنوان UUID

إذا كنت تتحكم في كلا الطرفين في واجهة برمجة التطبيقات، هناك خيار أوضح من حيث البنية لخلق الموارد: يُمكن للعميل تعيين هوية المورد ويستخدم PUT بدلًا من POST.

# Instead of this (POST, not idempotent):
POST /orders
{"amount": 99.00, "items": [...]}

# Do this (PUT to client-generated UUID, idempotent by spec):
PUT /orders/7f3b9c2a-4e5d-4f8b-9a1c-2d3e4f5a6b7c
{"amount": 99.00, "items": [...]}

PUT هو مُتكرر بحسب معيار HTTP — "أعد هذا المورد إلى الحالة المحددة" يعني أن تكرار نفس PUT لا يُحدث تأثيرًا إضافيًا بعد أن أصبح المورد موجودًا. يُعالج الخادم التكرار بـ INSERT ... ON CONFLICT DO NOTHING أو مكافئ.

يُستخدم هذا النمط بشكل جيد في إنشاء الطلبات، إدارة المسودات، أو أي مورد حيث يُهم التوازن في الهوية عبر التكرارات. لا يُستخدم عندما يُخصص الهوية من طرف ثالث (مُعالج الدفع)، أو عندما يجب أن تحدث التأثيرات الجانبية على الخادم قبل أن تعرف الهوية.

ما يظهره ستريب في التنفيذ الفعلي

محتوى توثيق مفتاح التكرار في ستريب يمكن قراءته بالكامل. بعض التفاصيل المهمة في الممارسة: صيغة المفتاح:

  • أي سلسلة حتى 255 حرفًا، مُحددة لحساب ستريب الخاص بك. لا تؤثر مشاركة مفتاحين من حسابين مختلفين. نافذة 24 ساعة:
  • تنتهي مفاتيح بعد 24 ساعة. إذا تم إعادة التسجيل بعد انتهاء النافذة، يُعامل كطلب جديد — هذا مفيد لفهم ما إذا كنت تبني عمليات طويلة الأمد. تباين الجسم = 422:
  • نفس المفتاح، معلمات مختلفة → تُرجع ستريب 422 غير قابل للمعالجة. هذا هو السلوك الصحيح؛ يُكتشف الخطأ عندما تُستخدم مفتاح مُكرر بشكل غير مقصود. الإعادة التوازية للإخفاق:
  • إذا وصلت طلبات متماثلة في نفس المفتاح بشكل متوازي، تُعالج واحدة وتُرجع 409 الصراع على الأخرى. أعد المحاولة بعد فترة قصيرة. إخفاقات محفوظة:
  • إذا فشل الدفع (تم رفض البطاقة)، تُحفظ هذه الفشل. إعادة المحاولة مع نفس المفتاح تُرجع نفس الرفض. تحتاج إلى مفتاح جديد لمحاولة بطاقة مختلفة — وهذا هو الصحيح، لأن المحاولة السابقة كانت عملية مقصودة تمامًا. اختبار التكرار دون مجموعات تكامل كاملة

أسرع طريقة لتأكيد السلوك هي إرسال طلب إلى نفس المفتاح مرتين وفحص أن الطلب الثاني يُرجع الرد المحفوظ دون تفعيل التأثيرات الجانبية مرة أخرى.

مُولد أوامر cURL من IO Tools يُسهل إنشاء الطلب مع عناصر مخصصة — بما في ذلك — دون تذكر نمط أوامر curl. Idempotency-Key السيناريوهات التي يجب تغطيتها:

نفس المفتاح + نفس الجسم، داخل النافذة الزمنية → يُرجع الرد المحفوظ، يُفعّل التأثيرات الجانبية مرة واحدة

  • نفس المفتاح + معلمات مختلفة → 422 (أو الحالة المختارة للصراع)
  • نفس المفتاح بعد انتهاء النافذة الزمنية → يُعامل كطلب جديد
  • لا يُقدّم مفتاح → يتم معالجته بشكل طبيعي (حدد مسبقًا ما إذا كان المفتاح مطلوبًا أم اختياريًا)
  • طلبان متوازيان مع نفس المفتاح → يُعالج واحد، يُرجع 409 على الآخر
  • POST ليس مُتكررًا، والفراغ بين "إرسال الطلب" و"استلام الرد" هو المكان الذي يعيش فيه التأثيرات الجانبية المكررة. الحل ليس معقدًا: اولّد UUID قبل أي دورة إعادة التسجيل، وارسله كرأس، واحتفظ بالرد من جانب الخادم مُحدّدًا بذات الرأس. الأجزاء الصعبة هي التفاصيل — مثل حفظ الفشل، التعامل مع التكرارات المتوازية، اختيار فترة انتقال منطقية — لكن النمط الأساسي بسيط جدًا ويمكن إضافته إلى أي طلب مهم.

النسخة المختصرة

أضف دعم مفتاح التكرار قبل أن تصل الاتصالات الإنتاجية لأول مرة. إضافة هذا بعد حدوث دفع مكرر هو وقت سيئ جدًا لتعلم الدروس.

التكرار في واجهات برمجة التطبيقات — لماذا تم إرسال POST مرتين وطريقة إصلاحه 2

هل تريد حذف الإعلانات؟ تخلص من الإعلانات اليوم

تثبيت ملحقاتنا

أضف أدوات IO إلى متصفحك المفضل للوصول الفوري والبحث بشكل أسرع

أضف لـ إضافة كروم أضف لـ امتداد الحافة أضف لـ إضافة فايرفوكس أضف لـ ملحق الأوبرا

وصلت لوحة النتائج!

لوحة النتائج هي طريقة ممتعة لتتبع ألعابك، يتم تخزين جميع البيانات في متصفحك. المزيد من الميزات قريبا!

إعلان · حذف؟
إعلان · حذف؟
إعلان · حذف؟

ركن الأخبار مع أبرز التقنيات

شارك

ساعدنا على الاستمرار في تقديم أدوات مجانية قيمة

اشتري لي قهوة
إعلان · حذف؟