من المحتمل أن ترسل واجهة برمجة التطبيقات (API) الخاصة بك نفس بيانات JSON آلاف المرات يوميًا. يطلب العميل GET /v1/products/42، ويستقبل 18 كيلوبايت، ثم يطلبها مرة أخرى بعد خمس دقائق، ويستقبل نفس الـ 18 كيلوبايت. لم يتغير شيء. لقد دفعت ثمن النطاق الترددي والتسلسل وقراءة قاعدة البيانات على أي حال.
لقد حل HTTP هذه المشكلة بالفعل. يخبر رأس Cache-Control العملاء بمدة صلاحية الاستجابة. يمنحهم رأس ETag بصمة إصبع للتحقق مما إذا كانت قد تغيرت. معًا، يحولان الطلبات المتكررة إلى استجابات 304 Not Modified بأجسام فارغة، ويمكنهما حماية عمليات الكتابة الخاصة بك من التحديثات المفقودة كمكافأة. نفس الأفكار تدعم أنماط جانب العميل أيضًا؛ إذا كنت قد قرأت دليلنا حول تخزين استجابات API مؤقتًا في React، فهذا هو الجانب الخادم من تلك القصة.
يستعرض هذا الدليل الطبقات الثلاث للتخزين المؤقت لـ HTTP، ويوضح رحلة 304 ذهابًا وإيابًا خطوة بخطوة، ويفك التشابك بين no-cache مقابل no-store، وينتهي بكود Express عملي. سترى أيضًا كيفية التحقق من كل ذلك في Apidog عن طريق إرسال رؤوس مشروطة والتحقق من 304 بنفسك.
الطبقات الثلاث للتخزين المؤقت لـ HTTP
ينقسم التخزين المؤقت لـ HTTP لواجهات برمجة التطبيقات (APIs) إلى ثلاثة قرارات منفصلة. تقع الفرق في المشاكل عندما يخلطون بينها.
الطبقة 1: الحداثة (Freshness). كم من الوقت يمكن للعميل إعادة استخدام الاستجابة دون أن يطلبها منك على الإطلاق؟ هذا هو Cache-Control: max-age=60. لمدة 60 ثانية، يقدم العميل النسخة المخزنة مؤقتًا محليًا. لا يوجد حركة مرور للشبكة. هذا هو أرخص وصول ممكن للذاكرة المخبئية (cache hit) وهو أيضًا الأكثر خطورة، لأن العميل لا يمكنه اكتشاف التغيير حتى انتهاء المؤقت.
الطبقة 2: التحقق (Validation). بمجرد أن تصبح الاستجابة قديمة، لا يحتاج العميل إلى إعادة تنزيلها. يسأل "هل تغير هذا؟" عن طريق إرسال بصمة الإصبع التي أعطيتها له سابقًا. إذا لم يتغير المورد، فإنك تجيب بـ 304 Not Modified وبدون جسم. ETag مع If-None-Match هو الإصدار الدقيق لذلك؛ Last-Modified مع If-Modified-Since هو الإصدار الأقدم المستند إلى الطابع الزمني بدقة ثانية واحدة.
الطبقة 3: الإبطال (Invalidation). عندما تتغير البيانات، كيف تموت النسخ القديمة؟ تنتهي صلاحية ذاكرات التخزين المؤقت للعملاء الخاصين من تلقاء نفسها عبر max-age. تحتاج ذاكرات التخزين المؤقت المشتركة وشبكات CDN إلى عمليات مسح صريحة، أو مدد صلاحية قصيرة (TTLs)، أو توجيهات مثل stale-while-revalidate التي تحد من التقادم.
توفر الحداثة أقصى قدر، ويلتقط التحقق كل ما فاتته الحداثة، ويحافظ الإبطال على صدق كليهما. تحتاج معظم واجهات برمجة التطبيقات إلى الثلاثة.
كيف تعمل رحلة 304 Not Modified ذهابًا وإيابًا
إليكم الدورة الكاملة لنقطة نهاية منتج، خطوة بخطوة.
الطلب الأول. العميل لا يمتلك أي شيء مخزن مؤقتًا:
GET /v1/products/42 HTTP/1.1
Host: api.example.com
الاستجابة الأولى. تعيد الجسم بالإضافة إلى بيانات تعريف التخزين المؤقت:
HTTP/1.1 200 OK
Cache-Control: private, max-age=60
ETag: "33a64df551425fcc55e4d42a148795d9f2"
Content-Type: application/json
Content-Length: 18432
يخزن العميل الجسم و ETag. لمدة 60 ثانية التالية لا يتصل بك على الإطلاق.
الطلب الثاني، بعد 60 ثانية. النسخة قديمة، لذا يقوم العميل بإعادة التحقق:
GET /v1/products/42 HTTP/1.1
Host: api.example.com
If-None-Match: "33a64df551425fcc55e4d42a148795d9f2"
الاستجابة الثانية، المورد لم يتغير. يقارن الخادم ETag الوارد مع الـ ETag الحالي. إذا تطابقا، فـ:
HTTP/1.1 304 Not Modified
Cache-Control: private, max-age=60
ETag: "33a64df551425fcc55e4d42a148795d9f2"
لا يوجد جسم. بدلًا من 18 كيلوبايت، تكون الاستجابة بضع مئات من البايتات من الرؤوس. يقوم العميل بتحديد نسخته المخزنة مؤقتًا على أنها جديدة لمدة 60 ثانية أخرى ويقدمها. إذا كان المنتج قد تغير، فستعيد استجابة 200 OK عادية مع الجسم الجديد و ETag جديد. لقد غطينا رمز الحالة نفسه بمزيد من العمق في شرحنا لـ 304 Not Modified؛ النسخة المختصرة هي أن 304 هي تعليمات تخزين مؤقت، وليست خطأ.
الاقتصاديات بسيطة. طلب GET المشروط لا يزال يكلف رحلة ذهابًا وإيابًا بالإضافة إلى أي عمل يحسب الـ ETag الحالي. ما يلغيه هو نقل الحمولة وإعادة تحليلها من جانب العميل. بالنسبة لنقاط نهاية القائمة الكبيرة التي تستطلعها عملاء الجوال، يقلل هذا بشكل روتيني من حركة خروج API بنسبة 60 إلى 90 بالمائة.
توجيهات Cache-Control المهمة لواجهات برمجة التطبيقات
يحتوي Cache-Control على أكثر من اثني عشر توجيهًا. بالنسبة لواجهات برمجة تطبيقات JSON، تحمل خمسة منها معظم الأهمية.
no-storeمقابلno-cache. هذا هو الخطأ الأكثر شيوعًا في التخزين المؤقت في واجهات برمجة التطبيقات الإنتاجية، وهو يعمل في كلا الاتجاهين.no-storeيعني "لا تكتب هذا أبدًا إلى أي ذاكرة تخزين مؤقت". استخدمه للحمولات الحساسة حقًا: الرموز، البيانات المصرفية، معلومات التعريف الشخصية (PII) التي يجب ألا تستمر.no-cacheيعني تقريبًا عكس ما يبدو عليه: يمكن لذاكرات التخزين المؤقت تخزين الاستجابة، ولكن يجب عليها إعادة التحقق مع المصدر قبل كل إعادة استخدام. مقترنًا بـETag، يمنحكno-cacheتوفيرًا بـ 304 على كل طلب مع ضمان عدم عرض العملاء لبيانات قديمة أبدًا. الفرق التي تضعno-storeعلى كل شيء "لتكون آمنة" تقوم بتعطيل الطلبات الشرطية بالكامل وتدفع تكلفة الحمولة الكاملة في كل مكالمة.private. تحدد الاستجابة على أنها قابلة للتخزين المؤقت بواسطة عميل المستخدم النهائي فقط، وليس بواسطة ذاكرات التخزين المؤقت المشتركة أو شبكات CDN أبدًا. أي استجابة تختلف لكل مستخدم، وهي معظم حركة مرور API الموثقة، يجب أن تحملprivate. بدونها، يمكن لوكيل (proxy) تم تكوينه بشكل خاطئ أن يقدم بيانات حساب مستخدم لآخر.max-age. فترة صلاحية الحداثة بالثواني. بالنسبة لواجهات برمجة التطبيقات، فكر في قيم صغيرة: من 30 إلى 300 ثانية تغطي معظم نقاط نهاية القراءة. أنت لا تحاول إلغاء الطلبات ليوم كامل؛ أنت تحاول استيعاب الدفعات وحلقات الاستقصاء.stale-while-revalidate. الحل الوسط العملي.Cache-Control: max-age=60, stale-while-revalidate=300يخبر ذاكرات التخزين المؤقت: قدم النسخة القديمة لمدة تصل إلى 5 دقائق إضافية، ولكن قم بتحديثها في الخلفية. يحصل المستخدمون على استجابات فورية؛ يتم تحديث المصدر الخاص بك بعد ذلك بوقت قصير. تدعمه شبكات CDN مثل Cloudflare و Fastly، وكذلك المتصفحات.
إعداد افتراضي معقول لنقطة نهاية قراءة موثقة يبدو كالتالي:
Cache-Control: private, max-age=60, stale-while-revalidate=120
ETag: "9f8b2c41aa73e0d5"
المواصفات السلوكية الكاملة موجودة في RFC 9111، والذي حل محل RFC 7234 كوثيقة HTTP caching النهائية. عندما تتصرف شبكة CDN بطريقة تفاجئك، فإن هذا الـ RFC هو المكان الذي ستجد فيه الإجابة.
ETags القوية مقابل الضعيفة
يأتي الـ ETag بنوعين، ويفصل بينهما بادئة W/.
ETag قوي (ETag: "33a64df551425fcc") يعد بتطابق البايت بالبايت. استجابتان بنفس الـ ETag القوي تكونان متطابقتين، مما يجعل ETags القوية آمنة لطلبات نطاق البايت ومطلوبة للتحكم في التزامن مع If-Match.
ETag ضعيف (ETag: W/"33a64df551425fcc") يعد بتكافؤ دلالي. قد تختلف البايتات، ربما تغير ترتيب الحقول أو زادت قيمة حقل زمني، لكن المعنى هو نفسه، لذا يمكن لذاكرة التخزين المؤقت الاحتفاظ بنسختها.
أين يكمن الخطر في ذلك: برامج ضغط الوسائط. Nginx وبعض الأطر البرمجية تعيد كتابة ETags القوية إلى ضعيفة عندما تضغط الاستجابة فورًا (gzip)، لأن البايتات المضغوطة لم تعد تتطابق مع الأصل. إذا فشلت فحوصات التزامن الخاصة بك بشكل غامض خلف وكيل، فابحث عن بادئة W/ التي لم تكن موجودة عندما أرسل خادم التطبيق الخاص بك الاستجابة.
افتراضيًا، استخدم ETags قوية محسوبة على الجسم غير المضغوط. استخدم الضعيفة فقط عندما تقدم عن قصد تمثيلات متغيرة لنفس البيانات.
إنشاء ETags: تجزئة الجسم مقابل عمود الإصدار
هناك استراتيجيتان سائدتان، وتعتمد الاستراتيجية الصحيحة على مكان وجود التكلفة.
- تجزئة (Hash) جسم الاستجابة. قم بتسلسل الاستجابة، ثم قم بتجزئتها (MD5 أو SHA-1 جيد هنا؛ هذه بصمة إصبع وليست حدودًا أمنية)، ثم ضعها بين علامتي اقتباس. إنها دقيقة بطبيعتها ولا تحتاج إلى تغييرات في المخطط. المشكلة: تقوم ببناء الاستجابة الكاملة في كل طلب، بما في ذلك استجابات 304. أنت توفر النطاق الترددي ولكن ليس قدرة المعالجة أو حمل قاعدة البيانات.
- عمود الإصدار أو
updated_at. اشتق الـETagمن البيانات التي يمكنك جلبها بتكلفة منخفضة:ETag: "42-v17"من عداد إصدار الصف، أو تجزئةupdated_at. الآن يكلف الطلب الشرطي بحثًا واحدًا مفهرسًا بدلاً من التسلسل الكامل. المشكلة: يجب أن يرتفع الإصدار مع كل تغيير يؤثر على الاستجابة، بما في ذلك التغييرات في الجداول المرتبطة. إذا فاتك واحد، فستقدم استجابات 304 قديمة، وهذا أسوأ خطأ في التخزين المؤقت لأنه غير مرئي.
ابدأ بتجزئة الجسم. إنه صحيح افتراضيًا. انقل نقاط النهاية الساخنة إلى ETags المستندة إلى الإصدار عندما تظهر التحليلات أن تكلفة التسلسل مهمة.
ETags للتزامن المتفائل: If-Match و 412
تمنع بصمة الإصبع نفسها التي توفر النطاق الترددي عند القراءة، التحديثات المفقودة عند الكتابة.
مشكلة التحديث المفقود: يقوم اثنان من المديرين بتحميل المنتج 42 في نفس الوقت. يغير المدير أ السعر ويحفظ. يصحح المدير ب خطأ إملائي ويحفظ بعد 30 ثانية، فيقوم بالكتابة فوق تغيير السعر الخاص بـ أ بالسعر القديم الذي قام بـ ب بتحميله. لا يرى أحد أي خطأ. البيانات خاطئة بصمت.
الحل هو جعل كل تحديث مشروطًا بالإصدار الذي رآه العميل آخر مرة:
PUT /v1/products/42 HTTP/1.1
If-Match: "33a64df551425fcc55e4d42a148795d9f2"
Content-Type: application/json
يقارن الخادم If-Match بـ ETag الحالي للمورد. إذا تطابق: طبق التحديث، أعد 200 OK مع ETag جديد. إذا لم يتطابق، فقد وصل شخص آخر أولاً: ارفض بـ 412 Precondition Failed ولا تلمس البيانات. يقوم العميل بعد ذلك بإعادة الجلب، وإعادة تطبيق تغييره على الإصدار الجديد، وإعادة المحاولة. تذهب واجهات برمجة التطبيقات الصارمة إلى أبعد من ذلك وتعيد 428 Precondition Required على أي طلب PUT يهمل If-Match، مما يجعل فحص السلامة إلزاميًا.
هذا لا يكلفك شيئًا تقريبًا لإضافته بمجرد وجود ETags، ويحول خطأ تلف البيانات الصامت إلى حالة HTTP صريحة وقابلة لإعادة المحاولة.
ما تفعله شبكات CDN والوكلاء بهذه الرؤوس
تقع ذاكرات التخزين المؤقت المشتركة بين المصدر والعملاء، وتقرأ نفس الرؤوس وفقًا لقواعدها الخاصة.
privateيستثني الاستجابة من التخزين المؤقت لشبكة CDN بالكامل؛s-maxage=600يحدد مدة صلاحية خاصة بشبكة CDN أطول أو أقصر منmax-ageالخاص بالمتصفح.- تقوم معظم شبكات CDN بإعادة التحقق من المصدر الخاص بك باستخدام طلبات مشروطة. إذا أجاب المصدر الخاص بك على
If-None-Matchبـ304 Not Modified، فإن شبكة CDN تحدّث نسختها المخزنة دون سحب الجسم. تجعل ETags شبكة CDN الخاصة بك أرخص أيضًا. - تأكد دائمًا من أن إطار عملك يرسل
Varyبشكل صحيح. واجهة برمجة تطبيقات (API) تقدم كلاً من JSON و CSV من نفس عنوان URL تحتاج إلىVary: Accept، وإلا فإن ذاكرة التخزين المؤقت المشتركة ستقدم CSV لعميل JSON. - احترس من الوكلاء الذين يضعفون ETags عبر الضغط، كما هو موضح أعلاه.
مثال Express: إرجاع ETag والتعامل مع If-None-Match
يقوم Express بتعيين ETags ضعيفة من تلقاء نفسه، ولكن المعالجة اليدوية تمنحك ETags قوية بالإضافة إلى مسار الكتابة 412:
import crypto from "node:crypto";
import express from "express";
const app = express();
app.use(express.json());
function etagFor(payload) {
const hash = crypto.createHash("sha1")
.update(JSON.stringify(payload))
.digest("hex");
return `"${hash}"`;
}
app.get("/v1/products/:id", async (req, res) => {
const product = await db.products.find(req.params.id);
const etag = etagFor(product);
res.set("Cache-Control", "private, max-age=60, stale-while-revalidate=120");
res.set("ETag", etag);
if (req.get("If-None-Match") === etag) {
return res.status(304).end(); // fingerprint matches: no body
}
res.json(product);
});
app.put("/v1/products/:id", async (req, res) => {
const product = await db.products.find(req.params.id);
const currentEtag = etagFor(product);
const ifMatch = req.get("If-Match");
if (!ifMatch) {
return res.status(428).json({ error: "If-Match header required" });
}
if (ifMatch !== currentEtag) {
return res.status(412).json({ error: "Resource changed since you fetched it" });
}
const updated = await db.products.update(req.params.id, req.body);
res.set("ETag", etagFor(updated));
res.json(updated);
});
لاحظ أن فرع 304 لا يزال يرسل رؤوس Cache-Control و ETag. وفقًا لـ RFC 9111، يقوم 304 بتحديث بيانات التعريف للاستجابة المخزنة، لذا أعد إرسال أي شيء يحتاجه العميل للحفاظ على نسخته حديثة.
التحقق من سلوك التخزين المؤقت في Apidog
يمكن للتعليمات البرمجية التي تبدو صحيحة أن تخزن مؤقتًا بشكل خاطئ بمجرد دخول البرامج الوسيطة والوكلاء. اختبر على مستوى HTTP، وليس على مستوى التعليمات البرمجية.
في Apidog، يستغرق الفحص اليدوي حوالي دقيقة واحدة:
- أرسل
GET /v1/products/42وافتح لوحة رؤوس الاستجابة. تأكد من وجودETagوCache-Controlوأن الـETagمحاط بعلامات اقتباس. انسخ قيمة الـETag. - في نفس الطلب، أضف رأس
If-None-Matchبالقيمة المنسوخة وأرسله مرة أخرى. يجب أن تحصل على304 Not Modifiedبجسم فارغ. إذا كنت لا تزال تحصل على200 OK، فإن طبقة التحقق لديك لا تقارن بصمات الأصابع. - غيّر السجل، أرسل مرة أخرى، وتأكد من عودتك إلى
200 OKمعETagجديد.
للحفاظ على هذا العمل بعد كل عملية نشر، قم بتوصيل نفس التدفق في سيناريو اختبار. قم بربط طلبين: الأول يستخرج ETag من رؤوس الاستجابة إلى متغير، والثاني يعيده كـ If-None-Match ويؤكد أن الحالة تساوي 304 وأن الجسم فارغ. أضف خطوة ثالثة لمسار الكتابة: أرسل طلب PUT بقيمة If-Match قديمة متعمدة مثل "deadbeefcafe1234" وتأكد من حصولك على 412. يغطي دليلنا حول تأكيدات API بناء جملة التأكيد لأكواد الحالة والرؤوس.
قم بتشغيل هذا السيناريو في CI (التكامل المستمر) وسيصبح تحديث البرامج الوسيطة الذي يجرد ETags الخاصة بك بصمت خط أنابيب فاشلاً بدلاً من فاتورة نطاق ترددي. قم بتنزيل Apidog مجانًا وقم ببناء السيناريو مقابل نقاط النهاية الخاصة بك؛ يستغرق الأمر وقتًا أطول للقراءة عنه من النقر للتجميع.
الأسئلة الشائعة
ما الفرق بين no-cache و no-store؟
تمنع no-store التخزين المؤقت تمامًا: لا يتم كتابة أي شيء على القرص أو الذاكرة، لذا يقوم كل طلب بتنزيل الاستجابة بالكامل. تسمح no-cache بالتخزين ولكنها تفرض إعادة التحقق قبل كل إعادة استخدام، لذا عندما تقترن بـ ETag فإنها لا تزال تنتج استجابات 304 Not Modified وتوفر الحمولة. استخدم no-store للبيانات الحساسة فقط. استخدامها في كل مكان هو الخطأ الأكثر تكلفة في Cache-Control الذي يمكن أن يرتكبه فريق API.
هل تعمل ETags مع POST؟
في الغالب لا، وهذا مقصود. تصف ETags حالة مورد في عنوان URL، وعادةً ما يقوم POST بإنشاء شيء جديد بدلاً من قراءة حالة ثابتة. لا تقوم ذاكرات التخزين المؤقت بتخزين استجابات POST عمليًا. الرؤوس الشرطية المهمة لعمليات الكتابة هي If-Match على PUT و PATCH و DELETE، حيث يحمي ETag من التحديثات المفقودة. إذا كنت تميل إلى تخزين استجابات POST مؤقتًا، فهذه عادةً علامة على أن العملية يجب أن تكون GET.
هل استجابة 304 تجعل واجهة برمجة التطبيقات الخاصة بي أسرع؟
إنها تجعل عمليات النقل أصغر، وهو ليس نفس الشيء. لا يزال الخادم يتلقى الطلب، ويقوم بالمصادقة، ويحسب الـ ETag الحالي، لذا فإن توفير وحدة المعالجة المركزية (CPU) للمصدر يعتمد على مدى سهولة اشتقاق تلك البصمة. تظهر المكاسب في النطاق الترددي، وبطارية الهاتف المحمول، ووقت العرض على الشبكات البطيئة. قم بالقياس قبل وبعد؛ يوضح دليل اختبار أداء API الخاص بنا كيفية قياس زمن الوصول والإنتاجية حتى تتمكن من إثبات الفرق بدلاً من التخمين.
هل يجب أن أستخدم ETag أم Last-Modified؟
أرسل كليهما عندما تستطيع. ETag أكثر دقة: فهو يلتقط التغييرات في أجزاء من الثانية والاختلافات على مستوى المحتوى التي يفوتها الطابع الزمني، ويأخذ If-None-Match الأولوية على If-Modified-Since عندما يصل كلاهما. يظل Last-Modified مفيدًا كخيار احتياطي للعملاء الأقدم وكطريقة استدلال تستخدمها بعض ذاكرات التخزين المؤقت لتقدير الحداثة. إذا كنت ترسل واحدًا فقط، فأرسل ETag.
