Idempotency & Safety
มีสองคุณสมบัติที่ตัดสินว่า request หนึ่งจะทำซ้ำได้อย่างปลอดภัยหรือไม่ สองตัวนี้สับสนกันง่ายมาก เรามาแยกให้ชัดกันก่อน
Safe กับ idempotent
หัวข้อที่มีชื่อว่า “Safe กับ idempotent”- Safe — request ไม่มีผลที่สังเกตได้ต่อ state ของ server ทำได้แค่อ่านอย่างเดียว ตัวอย่างเช่น
GET,HEADและOPTIONS - Idempotent — ส่ง request เดิม N ครั้งแล้ว server ลงเอยที่ state เดียวกับส่งครั้งเดียว
GET,HEAD,OPTIONS,PUTและDELETEเป็น idempotent ส่วนPOSTและ (โดยทั่วไป)PATCHไม่เป็น
ทุก method ที่ safe ล้วน idempotent แต่ไม่ใช่ทุก method ที่ idempotent จะ safe — DELETE เปลี่ยนแปลง state แต่ก็ยัง idempotent
flowchart TD
M{Method} --> GET[GET / HEAD: safe + idempotent]
M --> PUTDEL[PUT / DELETE: idempotent, not safe]
M --> POST[POST: neither]
M --> PATCH[PATCH: usually neither] ทำไมสองเรื่องนี้ถึงสำคัญ: การ retry
หัวข้อที่มีชื่อว่า “ทำไมสองเรื่องนี้ถึงสำคัญ: การ retry”เครือข่ายทำ response หายได้ เมื่อ client (หรือ proxy หรือ load balancer) ไม่ได้รับการตอบกลับ ก็มีสิทธิ์ยิงซ้ำ ถ้า method เป็น idempotent การ retry ก็ไม่มีอันตราย แต่ถ้าไม่ใช่ — อย่าง POST ที่ตัดเงินจากบัตร — การ retry แบบมืดบอดอาจตัดเงินซ้ำสองครั้ง
ทำให้ POST retry ได้อย่างปลอดภัย
หัวข้อที่มีชื่อว่า “ทำให้ POST retry ได้อย่างปลอดภัย”เมื่อคุณจำเป็นจริง ๆ ที่จะให้การสร้างที่ไม่ idempotent ทนต่อการ retry ได้ ให้ใช้ idempotency key: client ส่ง header Idempotency-Key ที่ไม่ซ้ำกันมา และ server จดจำผลลัพธ์ของ key นั้นไว้ เพื่อให้การส่งซ้ำคืนผลลัพธ์เดิมกลับไปแทนที่จะทำงานสองครั้ง รายละเอียดของกลไกนี้อยู่ใน Versioning & Caching
| Property | ความหมาย | Methods |
|---|---|---|
| Safe | ไม่เปลี่ยน state ของ server | GET, HEAD, OPTIONS |
| Idempotent | ทำซ้ำกี่ครั้งผลเดิม | GET, PUT, DELETE (+ safe methods) |
| Neither | อาจเปลี่ยน state และผลต่างกัน | POST, PATCH |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”POST ที่ไม่ Idempotent โดยไม่มี Guard อาการ:
- user กด submit form สองครั้ง — สร้าง order สองใบ
- network retry ทำให้ charge บัตรสองครั้ง
- ต้องการ idempotency key สำหรับ POST ที่ sensitive เช่น payment
PATCH ที่ไม่ Idempotent โดยตั้งใจ อาการ:
PATCH /accounts/\{id\}/balanceด้วย{ "increment": 100 }— ทำซ้ำ = บวกซ้ำ- client retry ทำให้ balance เพิ่มผิด
- ใช้ absolute value:
{ "balance": 1100 }แทน increment
💡 ตัวอย่างจากของจริง
Stripe API:
Idempotency-Keyheader สำหรับ POST /charges- ส่ง key เดิมซ้ำ — Stripe return response เดิม ไม่ charge ซ้ำ
- เก็บ key ใน client และ retry ได้อย่างปลอดภัย
AWS S3:
- PUT object เป็น idempotent — upload ไฟล์เดิมซ้ำ = overwrite ไม่ใช่ duplicate
- ทำให้ retry ปลอดภัยสำหรับ large file upload