التخزين المؤقت في HTTP

الذاكرة المؤقتة في HTTP (HTTP cache) تحتفظ بنسخة من الاستجابة، حتى يُجاب طلب لاحق للمورد نفسه دون رحلة كاملة إلى الخادم الأصلي (origin server)، أي الخادم الذي يملك المورد. للمتصفح ذاكرة مؤقتة خاصة به، أما الذاكرات المؤقتة المشتركة، مثل الخادم الوكيل (proxy) أو شبكة توصيل المحتوى (CDN)، فتقع بين مستخدمين كثيرين والخادم. ويحدّد المعيار RFC 9111 القواعد، ويضبطها الخادم بترويسات الاستجابة.

مثال: صفحة منتج تستدعي GET /api/products/1042، وتحمل الاستجابة Cache-Control: max-age=60 وETag: "p1042-v7". خلال ⁦60⁩ ثانية يعيد المتصفح استخدام نسخته دون أن يسأل الخادم. بعدها يسأل هل ما زالت p1042-v7 هي النسخة الحالية؛ فإن كانت كذلك، ردّ الخادم بـ 304 Not Modified دون جسم.

لماذا يهم

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

كثيراً ما تخلو بيئات الاختبار من شبكة CDN، وكثيراً ما يعمل المختبِرون وخيار «Disable cache» في أدوات المطوّرين مفعّل، فتظهر هذه العيوب في بيئة الإنتاج ما لم يقرأ أحد ترويسات التخزين المؤقت قبل الإصدار.

حديثة أم متقادمة: ما تقرّره Cache-Control

تخزّن الذاكرة المؤقتة الاستجابة تحت مفتاح تخزين (cache key) يتكوّن على الأقل من طريقة الطلب والعنوان (URL)، ومعظم ما يُخزَّن استجابات ناجحة لطلبات GET. وما دامت الاستجابة المخزّنة حديثة (fresh) يجوز للذاكرة إعادة استخدامها دون الاتصال بالخادم، فإذا صارت متقادمة (stale) فعليها عادةً أن تتحقق من الخادم أولاً.

يأتي عمر الحداثة (freshness lifetime) من max-age، وإلا فمن الترويسة الأقدم Expires التي تحمل تاريخاً محدداً وتُتجاهَل إذا وُجد max-age. وإذا غاب الاثنان جاز للذاكرة أن تقدّر عمراً بنفسها (heuristic freshness)، غالباً من Last-Modified. لذلك فالاستجابة التي لا تحمل ترويسات تخزين ليست «غير مخزّنة»، بل سلوكها غير متوقع فحسب.

  • max-age=600: حديثة لمدة ⁦600⁩ ثانية. وs-maxage يحدّد العمر للذاكرات المشتركة وحدها.
  • no-cache: يجوز تخزينها، لكن يجب إعادة التحقق منها مع الخادم قبل كل إعادة استخدام. وهي لا تمنع التخزين رغم اسمها.
  • no-store: لا يجوز لأي ذاكرة مؤقتة تخزين الطلب أو الاستجابة. وهو التوجيه المناسب للبيانات الشخصية والحساسة.
  • private: لا يخزّنها إلا الذاكرة الخاصة بالمستخدم نفسه. وpublic: يجوز لأي ذاكرة تخزينها، حتى حيث لا تُخزَّن عادةً.
  • must-revalidate: بعد التقادم لا تُستخدم النسخة دون تحقق ناجح، حتى لو تعذّر الوصول إلى الخادم.
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=60
ETag: "p1042-v7"
Last-Modified: Wed, 30 Sep 2026 08:00:00 GMT
Date: Thu, 01 Oct 2026 09:00:00 GMT

جرّبها: محلّل ترويسات التخزين المؤقت جرّبها: منشئ ترويسات HTTP

إعادة التحقق: ETag وLast-Modified و304

لفحص النسخة المتقادمة بتكلفة قليلة، يضع الخادم على كل استجابة قيمة تحقق (validator): إما ETag، وهو وسم إصدار مبهم مثل "p1042-v7"، وإما Last-Modified، وهو تاريخ. وتعيد الذاكرة المؤقتة هذه القيمة في طلب شرطي (conditional request): If-None-Match مع ETag، أو If-Modified-Since مع التاريخ. وإذا حضر الاثنان، يوجّه RFC 9110 الخادم إلى تجاهل If-Modified-Since.

إذا لم يتغيّر شيء ردّ الخادم بـ 304 Not Modified: بلا جسم، بل بترويسات فقط مثل ETag وCache-Control وDate، تُحدَّث بها النسخة المخزّنة التي يعرضها العميل بعد ذلك. وإذا تغيّر المورد ردّ بـ 200 OK مع الجسم الجديد وقيمة ETag جديدة. والـ ETag الذي يبدأ بـ W/ ضعيف (weak): يَعِد بمحتوى مكافئ، لا بمحتوى مطابق بايتاً ببايت.

يُعاد التحقق من النسخة المتقادمة بترويسة If-None-Match: إذا بقيت قيمة ETag مطابقة ردّ الخادم بـ 304 Not Modified بلا جسم، وإذا تغيّر المنتج أرسل 200 OK مع الجسم الجديد وقيمة ETag جديدة.
  • المنتج لم يتغيّر

    • المتصفح ذاكرة مؤقتة خاصة
    • الخادم الأصلي shop.example
    1. 1 من المتصفح إلى الخادم الأصلي: الطلب الأول GET /api/products/1042
    2. 2 من الخادم الأصلي إلى المتصفح: ردّ 200 OK مع الجسم وقيمة تحقق ردّ Cache-Control: max-age=60 ETag: "p1042-v7"
    3. 3 المتصفح داخليًا: يعيد استخدام النسخة المخزّنة دون سؤال الخادم حديثة لمدة ⁦60⁩ ثانية: لا يُرسَل أي طلب
    4. 4 من المتصفح إلى الخادم الأصلي: النسخة متقادمة: طلب شرطي If-None-Match: "p1042-v7"
    5. 5 الخادم الأصلي داخليًا: يقارن قيمة ETag: ما زالت حالية
    6. 6 من الخادم الأصلي إلى المتصفح: ردّ 304 Not Modified بلا جسم ردّ تُحدِّث Date وCache-Control الجديدتان النسخة المخزّنة نجاح

    النتيجة: يعرض المتصفح الجسم الموجود لديه، وتبقى النسخة حديثة ⁦60⁩ ثانية أخرى.

  • المنتج تغيّر

    • المتصفح ذاكرة مؤقتة خاصة
    • الخادم الأصلي shop.example
    1. 1 من المتصفح إلى الخادم الأصلي: النسخة متقادمة: طلب شرطي If-None-Match: "p1042-v7"
    2. 2 الخادم الأصلي داخليًا: يقارن قيمة ETag: السعر تغيّر الإصدار الحالي هو p1042-v8
    3. 3 من الخادم الأصلي إلى المتصفح: ردّ 200 OK مع الجسم الجديد ردّ ETag: "p1042-v8"
    4. 4 المتصفح داخليًا: يستبدل النسخة المخزّنة

    النتيجة: يظهر السعر الجديد، ويحمل الطلب الشرطي التالي القيمة "p1042-v8".

GET /api/products/1042 HTTP/1.1
Host: shop.example
If-None-Match: "p1042-v7"

HTTP/1.1 304 Not Modified
Cache-Control: max-age=60
ETag: "p1042-v7"
Date: Thu, 01 Oct 2026 09:01:05 GMT

جرّبها: مولّد أوامر cURL جرّبها: محلّل ترويسات HTTP

الذاكرات المؤقتة المشتركة والخاصة

الذاكرة المؤقتة الخاصة (private cache) تخدم مستخدماً واحداً، وهي عادةً المتصفح. أما المشتركة (shared cache) فتخزّن الاستجابات لمستخدمين كثيرين: خادم وكيل، أو شبكة CDN، أو خادم وكيل عكسي (reverse proxy) أمام التطبيق. ويحمي RFC 9111 حالة واحدة افتراضياً: لا يجوز للذاكرة المشتركة إعادة استخدام استجابة لطلب حمل ترويسة Authorization إلا إذا سمحت الاستجابة بذلك، مثلاً بـ public أو s-maxage.

أما ملف تعريف ارتباط الجلسة (session cookie) فلا يحظى بهذه الحماية، لذا يجب أن تقول الاستجابة المخصّصة للمستخدم بنفسها private أو no-store. في درس المصادقة والتفويض (Authentication and Authorization) تحققت ممن يحق له رؤية أي بيانات؛ والذاكرة المشتركة طريق آخر قد يرى منه الشخص الخطأ تلك البيانات.

  • Vary تذكر ترويسات الطلب التي تعتمد عليها الاستجابة، مثل Vary: Accept-Language. ومن دونها قد تقدّم الذاكرة المشتركة الصفحة العربية لمن طلب الإنجليزية.
  • الطلب الناجح بالطريقة POST أو PUT أو DELETE يُبطل النسخة المخزّنة لذلك العنوان في الذاكرات التي يمرّ بها فقط؛ أما غيرها فتحتفظ بنسختها حتى تتقادم أو تُمسح صراحةً (purge).
  • وجود ترويسة Age يعني أن الاستجابة جاءت من ذاكرة مؤقتة. وكثير من شبكات CDN تضيف ترويسة خاصة بها تبيّن هل وُجدت الاستجابة في الذاكرة أم لا (hit أو miss)، ويختلف اسمها باختلاف المزوّد.

مثال تطبيقي: اختبار صفحة منتج خلف شبكة CDN

متجر يقدّم صفحات منتجاته عبر شبكة CDN. والقواعد: يظهر السعر الجديد خلال دقيقة واحدة، ولا تُخزَّن صفحات الحساب أبداً. حوّل القواعد إلى ترويسات متوقعة لكل نوع من الاستجابات، ثم اختبر السلوك بطلبات قابلة للتكرار (curl أو أدوات API أو لوحة Network في أدوات المطوّرين)، وسجّل هل فُتحت كل صفحة برابط أم بإعادة تحميل عادية أم بإعادة تحميل كاملة (hard reload).

  • ملف يحمل الإصدار في اسمه مثل app.3f9c1a.js: قيمة max-age الطويلة صحيحة، لأن كل إصدار يحصل على اسم ملف جديد.
  • واجهة المنتج مع max-age=60 وETag: كرّر الطلب مع If-None-Match وتوقّع 304 بلا جسم. غيّر السعر وانتظر ⁦60⁩ ثانية وكرّر: توقّع 200 مع السعر الجديد وقيمة ETag جديدة.
  • عبر شبكة CDN: أرسل الطلب نفسه مرتين وتحقق من ظهور Age وتزايدها. ثم أرسله مع Accept-Language: ar ثم مع en وقارن الجسمين.
  • صفحات الحساب: توقّع no-store. افتح عنوان الطلبات نفسه بصفتك العميل أ وبصفتك العميل ب في متصفحين؛ ويجب ألا يرى ب طلبات أ أبداً. وبعد تسجيل الخروج يجب ألا يعرض زر الرجوع بيانات خاصة؛ اختبر ذلك في كل متصفح مستهدف، لأن RFC 9111 لا يُلزم سجل التصفح باتباع قواعد التخزين المؤقت.

جرّبها: مدقّق ترويسات API

أخطاء شائعة

  • فهم no-cache على أنها «لا تخزّن»؛ فمعناها «أعد التحقق قبل إعادة الاستخدام»، وno-store هي التي تمنع التخزين.
  • افتراض أن الاستجابة التي لا تحمل ترويسات تخزين لا تُخزَّن أبداً؛ فقد تقدّر الذاكرة المؤقتة عمراً لها بنفسها.
  • الاختبار وخيار «Disable cache» مفعّل دائماً، أو في بيئة بلا شبكة CDN فقط، ثم الاستنتاج بأن التخزين المؤقت يعمل.
  • التحقق من وصول 304 دون التحقق من أن الصفحة تعرض بعدها المحتوى الحالي.
  • اختبار الصفحات المخصّصة للمستخدم بحساب واحد؛ فكشف تسرّب البيانات بين المستخدمين يحتاج إلى حسابين.
  • نسيان Vary حين تعتمد الاستجابة على اللغة أو على ترويسة طلب أخرى.

من منظور ضمان الجودة

يُضبط التخزين المؤقت في الشيفرة وفي خادم الويب وفي شبكة CDN، وغالباً على يد أشخاص مختلفين. أثناء تنقيح قائمة الأعمال (refinement) اسأل كم يجب أن تكون حداثة كل نوع من البيانات، وسجّل الإجابات بصيغة قابلة للاختبار، مثل: «الأسعار: لا يزيد عمرها على ⁦60⁩ ثانية؛ صفحات الحساب: لا تُخزَّن أبداً». هذه هي نتائجك المتوقعة.

في تقرير خطأ التخزين المؤقت اذكر ترويسات الطلب والاستجابة كاملة، والذاكرة التي أجابت (مع ترويسة Age أو ترويسة hit أو miss)، ووقت كل طلب، وطريقة تحميل الصفحة؛ وإلا فلن يعيد المطوّر إنتاج المشكلة حين يعيد التحميل بذاكرة فارغة. وأعد اختبار التخزين المؤقت بعد كل تغيير في إعدادات شبكة CDN.

تمرين عملي

راجع ترويسات التخزين المؤقت لمتجر إلكتروني

المطلوب

التبادلات الخمسة أدناه من متجر يعمل خلف شبكة CDN. والقواعد: لا يزيد عمر الأسعار على ⁦60⁩ ثانية، ولا يجوز لأي ذاكرة مؤقتة تخزين بيانات الحساب، وتأتي بيانات المنتج باللغة المطلوبة في Accept-Language. الاستجابة ⁦2⁩ هي صفحة المنتج التي تعرض السعر، وفي التبادل ⁦5⁩ لم يتغيّر المنتج.

قرّر هل التخزين المؤقت صحيح في كل استجابة، وصِف كل مشكلة تجدها.

المادة
1) GET /assets/app.3f9c1a.js
   -> 200 OK
      Cache-Control: public, max-age=31536000

2) GET /products/1042
   -> 200 OK
      Content-Type: text/html
      Cache-Control: public, max-age=86400

3) GET /api/account/orders
   Authorization: Bearer <access token>
   -> 200 OK
      Cache-Control: public, max-age=300

4) GET /api/products/1042
   Accept-Language: ar
   -> 200 OK
      Content-Language: ar
      Cache-Control: max-age=60
      ETag: "p1042-v7"
      Vary: Accept-Encoding

5) GET /api/products/1042
   If-None-Match: "p1042-v7"
   -> 200 OK
      ETag: "p1042-v7"
      Content-Length: 5120
الخطوات
  1. اكتب لكل استجابة من يجوز له تخزينها (المتصفح، الذاكرات المشتركة، لا أحد) ولأي مدة.
  2. قارن ذلك بالمحتوى وبالقواعد.
  3. اكتب لكل مشكلة الخطر، والترويسات المتوقعة، واختباراً واحداً يمرّ عبر الذاكرات المؤقتة الحقيقية.
إظهار الإجابة النموذجية
الإجابة النموذجية

الاستجابة ⁦1⁩ صحيحة: الإصدار جزء من اسم الملف، فكل إصدار جديد يغيّر العنوان، ولا يمكن لقيمة max-age مدتها سنة أن تقدّم شيفرة قديمة.

الاستجابة ⁦2⁩ عيب: يجوز لأي ذاكرة أن تعرض سعراً عمره حتى ⁦24⁩ ساعة. المتوقع: max-age=60، أو no-cache مع ETag. الاختبار: غيّر سعراً وافتح الصفحة عبر شبكة CDN بعد ⁦61⁩ ثانية.

الاستجابة ⁦3⁩ عيب أمني عالي الخطورة: public تسمح للذاكرة المشتركة بإعادة استخدام استجابة لطلب يحمل Authorization، فقد تعرض شبكة CDN طلبات عميل على عميل آخر. المتوقع: no-store. الاختبار: حسابان، والعنوان نفسه، عبر شبكة CDN؛ ويجب أن يرى كل منهما طلباته فقط.

الاستجابة ⁦4⁩ عيب: يعتمد الجسم على Accept-Language، وVary لا تذكرها، فقد تقدّم شبكة CDN الجسم العربي لطلب باللغة الإنجليزية. المتوقع: Vary: Accept-Encoding, Accept-Language. الاختبار: اطلب بالعربية ثم بالإنجليزية وقارن Content-Language.

الاستجابة ⁦5⁩ عيب: قيمة ETag مطابقة، فالرد الصحيح 304 Not Modified بلا جسم. وإعادة إرسال ⁦5120⁩ بايت تهدر عرض النطاق لكنها لا تعرض بيانات خاطئة، لذا خطورتها أقل من الاستجابة ⁦3⁩. وتخلو الاستجابة أيضاً من Cache-Control، فقد تقدّر الذاكرات المؤقتة عمرها بنفسها. المتوقع: 304 مع ETag وCache-Control: max-age=60 وقيمة Vary نفسها التي في الاستجابة ⁦4⁩ بعد إصلاحها. الاختبار: كرّر الطلب الشرطي وتوقّع 304.

أسئلة شائعة

ما الفرق بين no-cache وno-store؟

تسمح no-cache بتخزين الاستجابة لكنها تشترط إعادة التحقق مع الخادم قبل كل إعادة استخدام، فيحصل المستخدم دائماً على المحتوى الحالي، وغالباً عبر استجابة 304 قليلة التكلفة. أما no-store فتمنع تخزين الطلب والاستجابة كلياً. استخدم no-store للبيانات الشخصية أو الحساسة.

أيهما يرسل الخادم: ETag أم Last-Modified؟

كلاهما يصلح قيمة تحقق، وكثيراً ما يرسل الخادم الاثنين، لكن ETag أدق: دقة Last-Modified ثانية واحدة، وقد تتغيّر قيمتها حين يُعاد حفظ الملف بالمحتوى نفسه. وإذا حمل الطلب الشرطين معاً، يعتمد الخادم If-None-Match ويتجاهل If-Modified-Since.

كيف أختبر التخزين المؤقت إذا لم تكن في بيئة الاختبار شبكة CDN؟

افحص الترويسات هناك، لأن التطبيق هو من يضبطها. أما سلوك الذاكرة المشتركة (Age، ووجود الاستجابة في الذاكرة أو عدمه، وVary، وتسرّب البيانات بين المستخدمين) فاختبره في بيئة لها إعدادات CDN أو الخادم الوكيل نفسها التي في الإنتاج، مثل بيئة Staging. وإن لم توجد هذه البيئة، فسجّل هذه الفجوة في خطة الاختبار.

لماذا لا يرسل المتصفح أي طلب لبعض الملفات؟

لأنها حديثة: خلال مدة max-age يعيد المتصفح استخدام نسخته دون الاتصال بالخادم، وتُظهر لوحة Network أنها جاءت من الذاكرة المؤقتة. ويُرسَل الطلب الشرطي حين تكون النسخة متقادمة أو موسومة بـ no-cache، وقد تؤدي إعادة التحميل إلى إرساله أيضاً بحسب المتصفح.

المراجع

  • RFC 9111: HTTP CachingIETF · 2022كيف تحدد Cache-Control وExpires وETag والمتحقِّقات ما إذا كان يمكن تخزين الاستجابة وإعادة استخدامها.
  • RFC 9110: HTTP SemanticsIETF · 2022معاني أساليب HTTP وحقول الترويسات ورموز الحالة والتفاوض على المحتوى، بغضّ النظر عن إصدار البروتوكول.
  • HTTP caching — MDNMDN Web Docs (Mozilla)التخزين المؤقت الخاص مقابل المشترك، والحداثة، والتحقق عبر ETag / Last-Modified، وأنماط Cache-Control الشائعة.
  • OWASP Cheat Sheet Series: Session ManagementOWASP Foundationإنتروبيا معرّف الجلسة وسمات ملف تعريف الارتباط والانتهاء والتجديد بعد تسجيل الدخول ومعالجة تسجيل الخروج.

تدرّب بأدوات متقن

تحليل ترويسات طلبات واستجابات HTTP الخام إلى جدول منظّم مع الشروحات.

الويب

تكوين ترويسات HTTP باستخدام إعدادات مسبقة للتخزين المؤقت وCORS والأمان والتفاوض على المحتوى.

الويب

تحقّق من ترويسات استجابة API من حيث الصحة والأمان وأفضل ممارسات التخزين المؤقت.

اختبار واجهات API

إنشاء أوامر cURL من الطريقة وعنوان URL والترويسات والمتن وخيارات المصادقة.

الويب

للتعمّق أكثر

تقدّمك في هذا الموضوع

جزء من مسار تعلم ضمان الجودة