تخزين API المؤقت باستخدام ETag و Cache-Control: كيف تقلل الطلبات الشرطية من حجم حمولاتك

تعلم كيف يحول رأس Cache-Control والتحقق من ETag طلبات API المتكررة إلى استجابات 304، ويمنع التحديثات المفقودة باستخدام If-Match، ويقلل حجم الحمولة.

Ashley Goolam

Ashley Goolam

31 أغسطس 2026

تخزين API المؤقت باستخدام ETag و Cache-Control: كيف تقلل الطلبات الشرطية من حجم حمولاتك

Apidog للمؤسسات

النشر على الخوادم المحلية

SSO و RBAC

متوافق مع SOC 2

استكشف Apidog للمؤسسات

من المحتمل أن ترسل واجهة برمجة التطبيقات (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، تحمل خمسة منها معظم الأهمية.

إعداد افتراضي معقول لنقطة نهاية قراءة موثقة يبدو كالتالي:

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: تجزئة الجسم مقابل عمود الإصدار

هناك استراتيجيتان سائدتان، وتعتمد الاستراتيجية الصحيحة على مكان وجود التكلفة.

ابدأ بتجزئة الجسم. إنه صحيح افتراضيًا. انقل نقاط النهاية الساخنة إلى 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 والوكلاء بهذه الرؤوس

تقع ذاكرات التخزين المؤقت المشتركة بين المصدر والعملاء، وتقرأ نفس الرؤوس وفقًا لقواعدها الخاصة.

مثال 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، يستغرق الفحص اليدوي حوالي دقيقة واحدة:

  1. أرسل GET /v1/products/42 وافتح لوحة رؤوس الاستجابة. تأكد من وجود ETag و Cache-Control وأن الـ ETag محاط بعلامات اقتباس. انسخ قيمة الـ ETag.
  2. في نفس الطلب، أضف رأس If-None-Match بالقيمة المنسوخة وأرسله مرة أخرى. يجب أن تحصل على 304 Not Modified بجسم فارغ. إذا كنت لا تزال تحصل على 200 OK، فإن طبقة التحقق لديك لا تقارن بصمات الأصابع.
  3. غيّر السجل، أرسل مرة أخرى، وتأكد من عودتك إلى 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.

ممارسة تصميم API في Apidog

اكتشف طريقة أسهل لبناء واستخدام واجهات برمجة التطبيقات