التخزين المؤقت في 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 من المتصفح إلى الخادم الأصلي: الطلب الأول
GET /api/products/1042 - 2 من الخادم الأصلي إلى المتصفح: ردّ 200 OK مع الجسم وقيمة تحقق ردّ
Cache-Control: max-age=60ETag: "p1042-v7" - 3 المتصفح داخليًا: يعيد استخدام النسخة المخزّنة دون سؤال الخادم حديثة لمدة 60 ثانية: لا يُرسَل أي طلب
- 4 من المتصفح إلى الخادم الأصلي: النسخة متقادمة: طلب شرطي
If-None-Match: "p1042-v7" - 5 الخادم الأصلي داخليًا: يقارن قيمة ETag: ما زالت حالية
- 6 من الخادم الأصلي إلى المتصفح: ردّ 304 Not Modified بلا جسم ردّ تُحدِّث
DateوCache-Controlالجديدتان النسخة المخزّنة نجاح
النتيجة: يعرض المتصفح الجسم الموجود لديه، وتبقى النسخة حديثة 60 ثانية أخرى.
المنتج تغيّر
- المتصفح ذاكرة مؤقتة خاصة
- الخادم الأصلي
shop.example
- 1 من المتصفح إلى الخادم الأصلي: النسخة متقادمة: طلب شرطي
If-None-Match: "p1042-v7" - 2 الخادم الأصلي داخليًا: يقارن قيمة ETag: السعر تغيّر الإصدار الحالي هو
p1042-v8 - 3 من الخادم الأصلي إلى المتصفح: ردّ 200 OK مع الجسم الجديد ردّ
ETag: "p1042-v8" - 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مثال تطبيقي: اختبار صفحة منتج خلف شبكة 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 لا يُلزم سجل التصفح باتباع قواعد التخزين المؤقت.
أخطاء شائعة
- فهم
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 صحيحة: الإصدار جزء من اسم الملف، فكل إصدار جديد يغيّر العنوان، ولا يمكن لقيمة 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 Cachingكيف تحدد Cache-Control وExpires وETag والمتحقِّقات ما إذا كان يمكن تخزين الاستجابة وإعادة استخدامها.
- RFC 9110: HTTP Semanticsمعاني أساليب HTTP وحقول الترويسات ورموز الحالة والتفاوض على المحتوى، بغضّ النظر عن إصدار البروتوكول.
- HTTP caching — MDNالتخزين المؤقت الخاص مقابل المشترك، والحداثة، والتحقق عبر ETag / Last-Modified، وأنماط Cache-Control الشائعة.
- OWASP Cheat Sheet Series: Session Managementإنتروبيا معرّف الجلسة وسمات ملف تعريف الارتباط والانتهاء والتجديد بعد تسجيل الدخول ومعالجة تسجيل الخروج.
تدرّب بأدوات متقن
تحليل ترويسات Cache-Control وExpires وETag وLast-Modified وVary وAge، وشرح كيفية تخزين المتصفحات وشبكات CDN للاستجابة مؤقتاً.
تحليل ترويسات طلبات واستجابات HTTP الخام إلى جدول منظّم مع الشروحات.
تكوين ترويسات HTTP باستخدام إعدادات مسبقة للتخزين المؤقت وCORS والأمان والتفاوض على المحتوى.
تحقّق من ترويسات استجابة API من حيث الصحة والأمان وأفضل ممارسات التخزين المؤقت.
إنشاء أوامر cURL من الطريقة وعنوان URL والترويسات والمتن وخيارات المصادقة.
للتعمّق أكثر
تقدّمك في هذا الموضوع
سجّل الدخول لتتابع تقدّمك
علّم الموضوعات مكتملة، وتابع من حيث توقفت، وافتح امتحانات التخصص.