تعذر استئناف تنزيل النسخة الاحتياطية بعد الانقطاع لأن رمز البريد الإلكتروني تم استهلاكه بالفعل

تذكرت مشكلة مشابهة يبدو أنها تم إصلاحها في إصدار 2026.01.0-latest، لذا أعتقد أن هذه مشكلة حالية منفصلة تتعلق بالاسترداد من تنزيل نسخة احتياطية انقطع، وليس فشل التنزيل الأولي القديم.

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

لقد تمكنت الآن من إعادة إنتاج هذا السلوك بالضبط على إصدار Discourse الحالي.

إعادة الإنتاج

كنت أقوم بتنزيل نسخة احتياطية محلية لـ Discourse بحجم:

4,231,639,143 بايت

باستخدام Safari على iOS.

كان التنزيل يتم بشكل طبيعي. لدي تسجيل شاشة يُظهر زيادته من حوالي:

https://youtube.com/shorts/_Pjc-kxJ1OE?feature=shared

313.3 ميغابايت إلى 317.8 ميغابايت

ثم انتقلت من Safari لمدة حوالي 15 ثانية فقط.

عندما عدت إلى Safari، كان التنزيل قد توقف عند حوالي:

319.8 ميغابايت

وعرض Safari زر المحاولة مرة أخرى.


يُظهر سجل وصول Caddy للطلب الأصلي:

GET /admin/backups/...

HTTP/3

Range: none

status: 200

Content-Length: 4231639143

size: 319864024

ثم استخدمت زر المحاولة الخاص بـ Safari نفسه.

أصدر Safari طلب نطاق بايت HTTP حقيقي:

GET /admin/backups/...

HTTP/3

Range: bytes=319799904-

status: 422

لذا، لم يكن Safari يحاول ببساطة تنزيل الملف كاملاً مرة أخرى. كان يحاول بشكل صحيح استئناف الملف الموجود عند النقطة التي توقف عندها النقل السابق تقريباً.

رفض Discourse طلب النطاق (Range) ذلك برمز 422.

كود Discourse الحالي

بالنظر إلى Admin::BackupsController#show الحالي، تكون التسلسل الفعلي:

EmailBackupToken.compare(current_user.id, token)

        ↓

find backup

        ↓

EmailBackupToken.del(current_user.id)

        ↓

send_file

لذا، فإن الطلب المصادق عليه الأولي يستهلك رمز البريد الإلكتروني قبل أن يكتمل نقل البيانات الضخم (عدة جيجابايت) فعلياً.

إذا انقطع ذلك النقل لاحقاً، يقوم Safari بإصدار طلب آخر مثل:

Range: bytes=319799904-

لكن هذا الطلب يمر عبر /admin/backups/… مرة أخرى، وقد تم حذف رمز البريد الإلكتروني بالفعل.

يبدو أن هذا يفسر رمز الخطأ 422 الذي تم التقاطه.

لا يزال مطلوباً وجود جلسة مصادقة بالإضافة إلى رمز النسخة الاحتياطية، لذا لا أقترح ببساطة جعل الرمز الحالي قابلاً لإعادة الاستخدام بشكل غير محدود.

بدلاً من ذلك، أتساءل عما إذا كان ينبغي وجود طريقة محددة النطاق تسمح لنفس المسؤول المصادق عليه باستئناف تنزيل نفس النسخة الاحتياطية المصرح بها مسبقاً.

على سبيل المثال، قد يكون ذلك تفويض تنزيل قصير العمر مرتبطاً بنفس المستخدم والنسخة الاحتياطية، على الرغم من أن المطورين قد يكون لديهم طريقة أفضل للتعامل مع هذا.

لماذا أصبح من الأسهل إعادة إنتاج هذا على iOS

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

هناك على الأقل ثلاثة تقارير مستضافة على مجتمع Apple:

تقارير أقدم

«فشل التنزيلات في الخلفية في Safari بعد تحديث IOS 26» — أكتوبر 2025

Safari Background Downloads Failing After… - Apple Community

يُبلغ المستخدم عن فشل تنزيلات Safari الكبيرة عند تشغيلها في الخلفية بعد الترقية إلى iOS 26. كما قام بترقية iPhone 15 Pro إلى iOS 26 وأبلغ عن نفس السلوك.

«مشكلة تنزيل الخلفية في Safari iOS - الإلغاء عند التصغير» — ديسمبر 2025

Safari iOS Background Download Issue - Ca… - Apple Community

تم الإبلاغ عنها على iPhone 15 يعمل بنظام iOS 26.1. تبدأ التنزيلات بشكل طبيعي لكنها تُلغى بعد التحويل إلى تطبيق آخر أو تصغير Safari.

«لا يمكن لـ Safari إكمال تنزيل كبير» — أبريل 2026

تم الإبلاغ عنها على iPhone 17 Pro Max يعمل بنظام iOS 26.4. يُزعم أن التنزيلات الكبيرة تتوقف مؤقتاً بعد قفل الجهاز بفترة وجيزة، أو عند التبديل بين الشبكة الخلوية و Wi-Fi. يذكر المؤلف تحديداً وجود إشارة جيدة ويقول إن نفس التنزيلات تكتمل على أجهزة الكمبيوتر، Mac، وأندرويد.

لا أعتقد أن تلك التقارير المجتمعية كافية للقول إن Apple أكدت وجود تراجع (Regression) في iOS.

ومع ذلك، فهي متسقة مع تسجيل الشاشة الخاص بي: كانت النسخة الاحتياطية قيد التنزيل بشكل طبيعي، انتقلت بعيداً عن Safari لفترة وجيزة، وعندما عدت أصبح النقل منقطعاً.

النقطة المهمة من جانب Discourse مستقلة عن سبب حدوث الانقطاع:

انقطاع نقل النسخة الاحتياطية الكبيرة

        ↓

يحاول المتصفح استئناف النطاق (Range) المعتمد على المعايير

        ↓

تم استهلاك رمز النسخة الاحتياطية بالفعل

        ↓

يتلقى الاستئناف رمز 422

ملاحظات من جانب الخادم

لا يبدو أن هذا مشكلة عامة في مسار nginx/sendfile الحالي.

تم إكمال نفس مسار النسخة الاحتياطية الضخم (عدة جيجابايت) بنجاح:

  • من Firefox/Linux عبر HTTP/3؛ و
  • من نفس iPhone عبر Wi-Fi باستخدام HTTP/3.

يعلن nginx الحالي أيضاً عن دعم نطاقات البايت (byte-range).

لذا، لا أقترح أن HTTP/3 أو Caddy أو nginx غير قادرة جوهرياً على تسليم النسخة الاحتياطية.

في إعادة الإنتاج هذه على وجه الخصوص، انقطع النقل الأصلي بعد حوالي 320 ميغابايت، ثم أصدر Safari طلب نطاق (Range) يبدو صالحاً، وتم رفض ذلك الطلب الثاني بواسطة Discourse.

هل من المعقول أن يكون لتنزيل نسخة احتياطية مصرح بها مسبقاً آلية قصيرة العمر تسمح لنفس المسؤول المصادق عليه باستئناف نفس ذلك الملف بعد انقطاع النقل؟

راجعت سجلّات التطوير وتغطية الاختبارات الحالية للحصول على فكرة أوضح عما ينبغي الحفاظ عليه في أي إصلاح محتمل.

تعود سلوكية الاستخدام الواحد إلى تغيير تعزيز الأمان الذي تم في مارس 2017:

أدخل هذا الالتزام رموز تنزيل احتياطي عبر البريد الإلكتروني، كما استهلك الرمز فورًا بمجرد توثيق طلب التنزيل.

لا يزال الترتيب نفسه قائمًا في Admin::BackupsController#show الحالي:

كما لا تزال آلية تنفيذ الرمز الأساسية بسيطة جدًا:

تخزّن EmailBackupToken رمز Redis واحدًا لكل مستخدم مع انتهاء صلاحية بعد يوم واحد. إنها مرتبطة بالمستخدم بدلاً من أن تكون مرتبطة بنسخة احتياطية محددة.

عند النظر في تاريخ هذا الملف، يبدو أن التغييرات منذ عام 2017 كانت تغييرات آلية/أسلوبية وليس تغييرات في نموذج التفويض.

هذا يجعل ترك الرمز المرسل بالبريد الإلكتروني الحالي قابلًا لإعادة الاستخدام يبدو غير مرغوب فيه: فسيضعف قيد الاستخدام الواحد الأصلي، والرمز نفسه غير محدد بالنسخة الاحتياطية الموجودة في عنوان URL.

توجد اختبارات الطلبات الحالية هنا:

تغطي هذه الاختبارات تنزيلًا صالحًا أوليًا، ورموزًا غير صالحة، ومعرفات نسخ احتياطية غير صالحة/ناقصة والصلاحيات، لكنني لم أجد تغطية لطلب Range: بعد انقطاع النقل.

بالمثل، يوثّق مواصفة اختبار طلبات API مسار تنزيل اسم الملف + الرمز العادي:

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

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

السلوكيات التي سأهدف إلى الحفاظ عليها/إضافتها هي:

  • يبقى طلب GET الأولي برمز البريد الإلكتروني الصالح مسموحًا به؛
  • يبقى رفض أي GET كامل عادي آخر باستخدام رمز البريد الإلكتروني المستهلك؛
  • يمكن استئناف إعادة محاولة نطاق بايت لنفس النسخة الاحتياطية من قبل نفس المسؤول الموثق لفترة محدودة قصيرة؛
  • يبقى رفض محاولة استئناف ضد نسخة احتياطية أخرى؛
  • يبقى رفض تفويض استئناف منتهي الصلاحية.

لم أفترض أن هذا هو بالضرورة التنفيذ الذي سيميله المسؤولون عن المشروع، لكن نطاق الكود/الاختبارات يبدو محدودًا إلى حد ما.

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

لقد أنشأت الآن طلب سحب (PR) لهذا الغرض:

تحافظ هذه التنفيذ على رمز البريد الإلكتروني للاستخدام الواحد الحالي لبدء التنزيل، مع السماح للنسخ الاحتياطي المحلية بإنشاء ترخيص استئناف محدود النطاق مخصص لـ:

  • نفس المسؤول المصادَق عليه؛

  • نفس النسخة الاحتياطية؛

  • نفس قيمة الرمز الأصلي؛ و

  • طلب لاحق يحمل ترويسة Range.

لا يزال يتم رفض طلب GET كامل ثانوي عادي باستخدام الرمز المستهلك، وكذلك يتم رفض طلب Range لنسخة احتياطية أخرى. تحتفظ النسخ الاحتياطية البعيدة/لـ S3 بسلوكها الحالي.

جعلت فترة الاستئناف قائمة (enum) بدلاً من مدة غير محدودة:

  • معطل (Disabled)

  • ساعة واحدة

  • 6 ساعات (موصى به)

  • 12 ساعة

  • حتى انتهاء صلاحية رمز البريد الإلكتروني الأصلي

يتم أيضًا تحديد فترة الاستئناف الفعلية بحد أقصى يعادل العمر المتبقي لرمز البريد الإلكتروني الأصلي عند بدء التنزيل الأولي.

يبقى سلوك Discourse الحالي هو الافتراضي (معطل) من أجل التوافق مع الإصدارات السابقة، بينما يتم تقديم خيار 6 ساعات كخيار موصى به عند تمكين التنزيلات القابلة للاستئناف.

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

كما أضفت تغطية للرجوع (regression coverage) لمسار الاستئناف الناجح وحدود التفويض الرئيسية، بما في ذلك:

  • بقاء رفض إعادة استخدام الرمز العادي؛

  • بقاء رفض الاستئناف عبر نسخ احتياطية مختلفة؛

  • منع تعطيل الإعداد بشكل فوري من الاستئناف الإضافي؛

  • عدم حصول S3 على إذن استئناف محلي؛ و

  • إنتاج التنزيل الأولي واستئنافه لسجل تدقيق تنزيل نسخة احتياطية واحدة فقط.

تمر حزمة CI الكاملة، وطلب السحب (PR) جاهز للمراجعة.

سيكون من المرحب به تقديم ملاحظات حول نموذج الأمان، ولا سيما حول التوصية بـ 6 ساعات والإعداد الافتراضي.