1. ספרייה
  2. HTTP והרשת
  3. שיטות HTTP

עודכן לפני חודש

שלחת בקשה. חלף פסק הזמן. האם השרת עיבד אותה?

אתה באמת לא יודע. הבקשה אולי הגיעה והצליחה, רק התגובה אבדה. או שהבקשה מתה בדרך. הרשת לא אומרת לך אם הבקשה שלך הצליחה — היא אומרת לך אם קיבלת תשובה. אלה לא אותו דבר.

אידמפוטנטיות היא האופן שבו מתמודדים עם אי-ודאות זו.

מה פירוש אידמפוטנטיות

שיטת HTTP היא אידמפוטנטית אם ביצוע אותה בקשה מספר פעמים מייצר את אותה השפעה על השרת כמו ביצועה פעם אחת.

DELETE היא אידמפוטנטית. מחק משאב פעם אחת — הוא נעלם. מחק אותו עשר פעמים נוספות — הוא עדיין נעלם. התגובות שונות (הראשונה מחזירה 204 No Content, שאר מחזירות 404 Not Found), אבל התוצאה זהה: המשאב לא קיים.

POST בדרך כלל אינה אידמפוטנטית. שלח טופס שלוש פעמים — ייתכן שתיצור שלושה משאבים. חייב כרטיס אשראי שלוש פעמים — תחייב את הלקוח שלוש פעמים. כל בקשה משנה את מצב השרת בצורה חדשה.

השיטות האידמפוטנטיות

GET

אחזור משאב אינו משנה אותו. בקש את אותו מאמר מאה פעמים — מצב השרת נשאר ללא שינוי.

GET /articles/123 HTTP/1.1

→ תוכן המאמר (בפעם הראשונה)
→ אותו תוכן מאמר (בפעם המאה)

הערה: ממשקי API בעיצוב לקוי מוסיפים תופעות לוואי ל-GET — הגדלת מוני צפיות, הפעלת פעולות, כתיבת לוגים. זה מפר את מפרט HTTP ויוצר בעיות אמיתיות כאשר פרוקסים, דפדפנים ומאזני עומס מנסים שוב בקשות GET, בהנחה שהן בטוחות לחזרה.

PUT

עדכון משאב למצב מסוים הוא אידמפוטנטי. הגדר את כותרת המאמר ל-"Updated Title" פעם אחת — יש לו את הכותרת הזו. הגדר שוב — עדיין יש לו את הכותרת הזו.

PUT /articles/123 HTTP/1.1
{"title": "Updated Title", "content": "Updated content"}

→ המאמר עודכן (בפעם הראשונה)
→ המאמר כבר נמצא במצב הזה (בפעם השנייה)

DELETE

הסרת משאב היא אידמפוטנטית. המשאב נעלם לאחר הבקשה הראשונה. הוא עדיין נעלם לאחר העשירית.

DELETE /articles/123 HTTP/1.1

→ 204 No Content (בפעם הראשונה)
→ 404 Not Found (בפעם העשירית — אבל המשאב נעלם בשני המקרים)

HEAD ו-OPTIONS

HEAD מאחזר מטא-נתונים מבלי לשנות דבר. OPTIONS שואל אילו פעולות נתמכות. אף אחד מהם אינו משנה את מצב השרת.

השיטות הלא-אידמפוטנטיות

POST

כל POST עשוי ליצור מצב חדש:

POST /articles HTTP/1.1
{"title": "New Article"}

→ מאמר #123 נוצר (בקשה ראשונה)
→ מאמר #124 נוצר (בקשה שנייה)
→ מאמר #125 נוצר (בקשה שלישית)

זוהי בעיית ההגשה הכפולה. פסקי זמן ברשת, משתמשים חסרי סבלנות שלוחצים על "שלח" כמה פעמים, ניסיונות חוזרים אוטומטיים — כולם עלולים ליצור כפילויות לא מכוונות.

PATCH

PATCH יכולה ללכת בשני הכיוונים, תלוי בפורמט ה-patch.

אידמפוטנטית (הגדרת ערכים מוחלטים):

PATCH /users/123 HTTP/1.1
{"email": "new@example.com"}

→ האימייל הוא כעת new@example.com (בכל פעם)

לא-אידמפוטנטית (שינויים יחסיים):

PATCH /users/123 HTTP/1.1
{"balance": {"increment": 10}}

→ היתרה גדלה ב-10 (בכל פעם)

JSON Merge Patch (RFC 7396) היא אידמפוטנטית. JSON Patch (RFC 6902) עשויה להיות לא-אידמפוטנטית, תלוי בפעולות.

למה זה חשוב

ניסיונות חוזרים בטוחים

עם שיטות אידמפוטנטיות, אפשר לנסות שוב בחופשיות:

async function fetchArticle(id) {
  for (let attempt = 0; attempt < 3; attempt++) {
    try {
      return await fetch(`/articles/${id}`);
    } catch (error) {
      if (attempt === 2) throw error;
      await sleep(1000 * (attempt + 1));
    }
  }
}

GET היא אידמפוטנטית. ניסיון חוזר לא יגרום נזק.

עם POST, ניסיון חוזר הוא מסוכן:

// כל ניסיון חוזר עלול ליצור מאמר נוסף
async function createArticle(data) {
  for (let attempt = 0; attempt < 3; attempt++) {
    try {
      return await fetch('/articles', {
        method: 'POST',
        body: JSON.stringify(data)
      });
    } catch (error) {
      // פסק זמן — האם הצליח? ניסיון חוזר עלול ליצור כפילויות
    }
  }
}

מאזני עומס מנסים שוב אוטומטית

כאשר שרת backend אינו מגיב, מאזני עומס מנסים שוב בקשות אידמפוטנטיות על שרת אחר:

לקוח → מאזן עומס → שרת 1 (פסק זמן)
                   → שרת 2 (ניסיון חוזר — בטוח עבור GET/PUT/DELETE)

הם לא מנסים שוב POST ללא הגדרה מפורשת. הם יודעים שזה מסוכן.

דפדפנים מגנים על משתמשים

רענן דף לאחר הגשת טופס ותראה:

"האם אתה בטוח שברצונך לשלוח מחדש את הטופס הזה?"

דפדפנים יודעים ש-POST אינה אידמפוטנטית. הם מזהירים לפני חזרה על הפעולה.

מטמון עובד

רק שיטות אידמפוטנטיות יכולות להישמר במטמון בבטחה. אם בקשה משנה את מצב השרת, תגובות מהמטמון הופכות לשקר.

תגובות GET נשמרות במטמון כברירת מחדל. תגובות POST לא.

הפיכת POST לאידמפוטנטית

מכיוון ש-POST לעתים קרובות זקוקה לניסיונות חוזרים, מספר דפוסים מאפשרים להוסיף לה אידמפוטנטיות:

מפתחות אידמפוטנטיות

הלקוח מייצר מפתח ייחודי לכל פעולה לוגית:

const idempotencyKey = crypto.randomUUID();

fetch('/api/orders', {
  method: 'POST',
  headers: {
    'Idempotency-Key': idempotencyKey,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(orderData)
});

השרת עוקב אחרי המפתחות. אותו מפתח פעמיים? מחזיר את התוצאה המקורית:

app.post('/api/orders', async (req, res) => {
  const key = req.get('Idempotency-Key');
  const cached = await cache.get(key);
  
  if (cached) return res.json(cached);
  
  const order = await createOrder(req.body);
  await cache.set(key, order, { ttl: 3600 });
  res.status(201).json(order);
});

כעת ניסיונות חוזרים בטוחים.

מזהים שנוצרים על ידי הלקוח

תן ללקוח לספק את המזהה:

const orderId = crypto.randomUUID();

fetch(`/api/orders/${orderId}`, {
  method: 'PUT',
  body: JSON.stringify({ id: orderId, items: [...] })
});

PUT לכתובת URL מסוימת היא אידמפוטנטית. הזמנה קיימת? עדכן אותה. לא קיימת? צור אותה. ניסיון חוזר מייצר את אותה הזמנה בדיוק.

אסימונים חד-פעמיים

הטמן אסימון בטופס:

<form method="POST" action="/submit">
  <input type="hidden" name="token" value="unique-token-123">
</form>

השרת מאמת ומבטל את האסימון:

app.post('/submit', (req, res) => {
  if (!validateAndInvalidateToken(req.body.token)) {
    return res.status(400).send('Invalid or expired token');
  }
  processForm(req.body);
  res.redirect('/success');
});

הגשות כפולות נכשלות.

עיצוב RESTful

השתמש בשיטות לפי הסמנטיקה שלהן:

פעולות אידמפוטנטיות ← PUT או DELETE

  • הגדרת משאב למצב מסוים: PUT
  • הסרת משאב: DELETE

פעולות לא-אידמפוטנטיות ← POST

  • יצירה עם מזהים שנוצרים על ידי השרת: POST
  • פעולות שאין לחזור עליהן: POST
PUT /users/123/status HTTP/1.1
{"status": "active"}

אידמפוטנטי. הגדרת הסטטוס ל-"active" עשר פעמים משאירה אותו פעיל.

POST /users/123/notifications HTTP/1.1
{"message": "Welcome back!"}

לא אידמפוטנטי. שליחה פעמיים שולחת שתי הודעות.

בדיקת אידמפוטנטיות

ודא שהשיטות האידמפוטנטיות שלך אכן מתנהגות כך:

test('PUT /articles/:id is idempotent', async () => {
  const data = { title: 'Test', content: 'Content' };
  
  await request(app).put('/articles/123').send(data);
  await request(app).put('/articles/123').send(data);
  await request(app).put('/articles/123').send(data);
  
  const count = await countArticles({ title: 'Test' });
  expect(count).toBe(1);
});

טעויות נפוצות

GET עם תופעות לוואי: אל תשנה מצב בטיפולי GET. פרוקסים ודפדפנים ינסו שוב בקשות GET ללא אישור.

ניסיון חוזר עיוור של POST: ללא מפתחות אידמפוטנטיות או אסימונים, ניסיונות חוזרים יוצרים כפילויות.

הנחה ש-PATCH היא אידמפוטנטית: זה תלוי בפורמט ה-patch. פעולות הגדלה אינן אידמפוטנטיות.

שאלות נפוצות על אידמפוטנטיות של שיטות HTTP

מדוע DELETE מחזירה 404 בבקשות עוקבות אם היא אידמפוטנטית?

אידמפוטנטיות מתייחסת להשפעה על השרת, לא לתגובה. לאחר DELETE, המשאב לא קיים — בין אם מחקת אותו פעם אחת ובין אם עשר פעמים. ה-404 פשוט אומר "הוא לא כאן," וזה בדיוק המצב שרצית להגיע אליו. ההשפעה זהה; רק קוד התגובה שונה.

האם אני יכול לנסות שוב בבטחה כל בקשה שפג פסק הזמן שלה?

רק בקשות אידמפוטנטיות (GET, PUT, DELETE, HEAD, OPTIONS). עבור POST, תזדקק למפתחות אידמפוטנטיות או להגנה דומה. בלעדיהן, ניסיון חוזר של POST שפג פסק הזמן שלה עלול ליצור כפילויות — אינך יודע אם הבקשה המקורית הצליחה.

מה ההבדל בין שיטות אידמפוטנטיות לשיטות בטוחות?

שיטות בטוחות (GET, HEAD, OPTIONS) אינן משנות את מצב השרת כלל. שיטות אידמפוטנטיות עשויות לשנות מצב, אבל חזרה עליהן אינה משנה את התוצאה. DELETE היא אידמפוטנטית אך אינה בטוחה — היא משנה מצב, אבל ביצועה פעמיים יש את אותה השפעה כמו ביצועה פעם אחת.

כמה זמן כדאי לשמור מפתחות אידמפוטנטיות?

מספיק זמן כדי לאפשר ניסיונות חוזרים ולפצות על עיכובי רשת — בדרך כלל בין שעה ל-24 שעות, בהתאם לשימוש. Stripe משתמשת ב-24 שעות. עבור רוב ממשקי ה-API, מספר שעות מספיקות. זמן קצר מדי ועלולים ניסיונות חוזרים לחמוק; זמן ארוך מדי מבזבז שטח אחסון.

האם דף זה היה מועיל?

😔
🤨
😃