Idempotency
ลอง API requests ซ้ำอย่างปลอดภัยโดยไม่สร้าง charges, customers หรือทรัพยากรอื่นๆ ซ้ำ เรียนรู้วิธีใช้ idempotency keys เพื่อสร้างการเชื่อมต่อการชำระเงินที่เชื่อถือได้
ภาพรวม
ปัญหาเครือข่าย, timeouts และ server errors สามารถทำให้ API requests ล้มเหลวหรือผลลัพธ์ไม่แน่นอน Idempotency ช่วยให้คุณลอง requests ซ้ำได้อย่างปลอดภัยโดยไม่ต้องกังวลเรื่องการดำเนินการซ้ำซ้อน โดยการให้ Idempotency-Key header Omise รับประกันว่า request เดียวกันจะให้ผลลัพธ์เดียวกันแม้จะส่งหลายครั้ง
- เพิ่ม
Idempotency-Keyheader ให้ POST/PATCH requests - ใช้ key ที่ไม่ซ้ำกันต่อการดำเนินการ (แนะนำ UUID)
- key เดียวกันส่งคืนผลลัพธ์เดียวกัน (cache 24 ชั่วโมง)
- จำเป็นสำหรับการสร้าง charges และการดำเนินการเกี่ยวกับเงิน
- ป้องกันการเรียกเก็บเงินซ้ำเมื่อมีปัญหาเครือข่าย
Idempotency คืออะไร
Idempotency หมายความว่าการดำเนินการสามารถทำได้หลายครั้งและให้ผลลัพธ์เดียวกัน สำหรับการประมวลผลการชำระเงิน นี่เป็นเรื่องสำคัญ:
ไม่มี Idempotency
1. ส่ง charge request → Network timeout
2. สำเร็จหรือไม่? ไม่รู้ ควรลองใหม่ไหม?
3. ลองใหม่ → Charge ซ้ำ! ลูกค้าถูกเรียกเก็บเงินสองครั้ง 💥
มี Idempotency
1. ส่ง charge พร้อม idempotency key → Network timeout
2. ลองใหม่ด้วย key เดิม → ส่งคืนผลลัพธ์เดิม
3. ไม่มี charge ซ้ำ ✅
Idempotency ทำงานอย่างไร
- ส่ง request พร้อม
Idempotency-Keyheader - Omise ประมวลผล และบันทึกผลลัพธ์
- ลองใหม่ด้วย key เดิมภายใน 24 ชั่วโมง:
- Omise ส่งคืนผลลัพธ์ที่ cache ไว้
- ไม่มีการดำเนินการใหม่
- status code และ body ของ response เหมือนเดิม
ตัวอย่าง Flow
# Request แรก (network timeout)
curl https://api.omise.co/charges \
-X POST \
-u skey_test_...: \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-d "amount=100000" \
-d "currency=thb" \
-d "card=tokn_test_..."
# Response: (timeout - ไม่รู้ว่าสำเร็จหรือไม่)
# ลองใหม่ด้วย key เดิม
curl https://api.omise.co/charges \
-X POST \
-u skey_test_...: \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-d "amount=100000" \
-d "currency=thb" \
-d "card=tokn_test_..."
# Response: ส่งคืน charge เดิม (ไม่ใช่อันใหม่)
เมื่อไหร่ควรใช้ Idempotency
ใช้เสมอสำหรับ:
✅ การสร้าง Charges
POST /charges
สำคัญที่สุด - ป้องกันการเรี ยกเก็บเงินซ้ำ
✅ การสร้าง Customers
POST /customers
ป้องกันบันทึกลูกค้าซ้ำ
✅ การสร้าง Refunds
POST /charges/:id/refunds
ป้องกันการคืนเงินซ้ำ
✅ การสร้าง Transfers
POST /transfers
ป้องกันการจ่ายเงินซ้ำ
✅ POST Requests ทั้งหมด ใช้ idempotency keys สำหรับ POST requests ที่สร้างทรัพยากรทั้งหมด
✅ PATCH Requests การอัปเดตสามารถลองใหม่ได้อย่างปลอดภัยด้วย idempotency
ไม่จำเป็นสำหรับ:
❌ GET Requests การอ่านข้อมูลเป็น idempotent อยู่แล้ว (ไม่มีผลข้างเคียง)
❌ DELETE Requests การลบเป็น idempotent โดยธรรมชาติ (ลบสองครั้ง = ผลลัพธ์เดียวกัน)
Idempotency-Key Header
รูปแบบ Header
Idempotency-Key: <unique-string>
ข้อกำหนด Key
| ข้อกำหนด | คำอธิบาย |
|---|---|
| รูปแบบ | string ใดๆ ที่มีความยาวไม่เกิน 255 ตัวอักษร |
| ความเป็นเอกลักษณ์ | ต้องไม่ซ้ำกันต่อการดำเนินการ |
| ตัวอักษร | แนะนำตัวอักษรและตัวเลขพร้อม hyphens |
| ตัวพิมพ์เล็ก/ใหญ่ | แยกแยะ: key-1 ≠ KEY-1 |
| การหมดอายุ | เก็บไว้ 24 ชั่วโมง |
แนะนำ: ใช้ UUIDs
# รูปแบบ UUIDv4 ( แนะนำ)
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
ทำไมต้อง UUID?
- ✅ รับประกันความเป็นเอกลักษณ์
- ✅ ไม่มีความเสี่ยงการชนกัน
- ✅ รูปแบบมาตรฐาน
- ✅ มีในทุกภาษา
ตัวอย่างการใช้งาน
Ruby
require 'omise'
require 'securerandom'
Omise.api_key = ENV['OMISE_SECRET_KEY']
# สร้าง idempotency key ที่ไม่ซ้ำ
idempotency_key = SecureRandom.uuid
begin
charge = Omise::Charge.create({
amount: 100000,
currency: 'thb',
card: token
}, {
'Idempotency-Key' => idempotency_key
})
puts "สร้าง charge แล้ว: #{charge.id}"
rescue Omise::Error => e
if e.http_status >= 500 || e.message.include?('timeout')
# ลองใหม่ด้วย key เดิมอย่างปลอดภัย
charge = Omise::Charge.create({
amount: 100000,
currency: 'thb',
card: token
}, {
'Idempotency-Key' => idempotency_key # key เดิม!
})
else
raise
end
end
Python
import omise
import uuid
omise.api_secret = os.environ['OMISE_SECRET_KEY']
# สร้าง idempotency key ที่ไม่ซ้ำ
idempotency_key = str(uuid.uuid4())
try:
charge = omise.Charge.create(
amount=100000,
currency='thb',
card=token,
headers={'Idempotency-Key': idempotency_key}
)
print(f"สร้าง charge แล้ว: {charge.id}")
except omise.errors.BaseError as e:
if e.http_status >= 500:
# ลองใหม่ด้วย key เดิมอย่างปลอดภัย
charge = omise.Charge.create(
amount=100000,
currency='thb',
card=token,
headers={'Idempotency-Key': idempotency_key} # key เดิม!
)
else:
raise
Node.js
const omise = require('omise')({
secretKey: process.env.OMISE_SECRET_KEY
});
const { v4: uuidv4 } = require('uuid');
async function createChargeWithRetry(chargeData, maxRetries = 3) {
// สร้าง idempotency key ที่ไม่ซ้ำ
const idempotencyKey = uuidv4();
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const charge = await omise.charges.create({
...chargeData,
headers: {
'Idempotency-Key': idempotencyKey
}
});
console.log(`สร้าง charge แล้ว: ${charge.id}`);
return charge;
} catch (error) {
const isServerError = error.statusCode >= 500;
const isTimeout = error.code === 'ETIMEDOUT';
const isLastAttempt = attempt === maxRetries - 1;
if ((isServerError || isTimeout) && !isLastAttempt) {
// ลองใหม่ด้วย key เดิมอย่างปลอดภัย
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
throw error;
}
}
}
// การใช้งาน
createChargeWithRetry({
amount: 100000,
currency: 'thb',
card: 'tokn_test_...'
});
แนวทางปฏิบัติที่ดีที่สุดสำหรับ Retry Logic
1. ลองใหม่เฉพาะ Transient Errors
function isRetryable(error) {
// ลองใหม่สำหรับ server errors
if (error.statusCode >= 500) return true;
// ลองใหม่สำหรับ timeouts
if (error.code === 'ETIMEDOUT') return true;
if (error.code === 'ECONNRESET') return true;
// อย่าลองใหม่สำหรับ client errors
if (error.statusCode >= 400 && error.statusCode < 500) return false;
return false;
}
2. ใช้ Exponential Backoff
import time
import random
def exponential_backoff(attempt, base_delay=1, max_delay=60):
"""คำนวณ delay พร้อม jitter"""
delay = min(base_delay * (2 ** attempt), max_delay)
jitter = random.uniform(0, delay * 0.1) # เพิ่ม jitter 10%
return delay + jitter
อ้างอิงด่วน
Idempotency Header
Idempotency-Key: <unique-string>
เมื่อไหร่ควรใช้
| ประเภท Request | ใช้ Idempotency? |
|---|---|
| POST (สร้าง) | ✅ เสมอ |
| PATCH (อัปเดต) | ✅ แนะนำ |
| GET (อ่าน) | ❌ ไม่จำเป็น |
| DELETE | ❌ ไม่จำเป็น |
การหมดอายุของ Key
- เก็บไว้: 24 ชั่วโมงตั้งแต่ใช้ครั้งแรก
- หลังหมดอายุ: request ใหม่จะสร้างทรัพยากรใหม่
Decision Tree สำหรับ Retry
Request ล้มเหลว?
├─ ใช่ → ตรวจสอบประเภท error
│ ├─ 5xx Server Error → ลองใหม่ด้วย key เดิม
│ ├─ Network Timeout → ลองใหม่ด้วย key เดิม
│ ├─ 4xx Client Error → แก้ไข request, ใช้ key ใหม่
│ └─ Other Error → อย่าลองใหม่
└─ ไม่ → สำเร็จ!
ทรัพยากรที่เกี่ยวข้อง
ถัดไป: เรียนรู้วิธีจัดการการเปลี่ยนแปลงเวอร์ชันและรักษาความเข้ากันได้ย้อนหลังใน API Versioning
FAQ
จะเกิดอะไรขึ้นถ้าใช้ idempotency key เดียวกันกับพารามิเตอร์ที่ต่างกัน?
API จะส่งคืนผลลัพธ์ที่แคชไว้จากคำขอแรก โดยไม่คำนึงถึงพารามิเตอร์ใหม่ นี่เป็นพฤติกรรมที่ตั้งใจไว้เพื่อป้องกันการดำเนินการซ้ำซ้อน ควรใช้ key ที่ไม่ซ้ำกันสำหรับแต่ละการดำเนินการที่แตกต่างกัน
Idempotency key มีอายุการใช้งานนานเท่าใด?
Idempotency key มีอายุ 24 ชั่ วโมงนับจากคำขอแรก หลังจาก 24 ชั่วโมง การใช้ key เดิมจะสร้างทรัพยากรใหม่แทนที่จะส่งคืนผลลัพธ์ที่แคชไว้
ต้องใช้ idempotency key สำหรับ GET request หรือไม่?
ไม่ GET request มีความเป็น idempotent อยู่แล้วโดยธรรมชาติเนื่องจากเป็นการอ่านข้อมูลเท่านั้นโดยไม่สร้างหรือแก้ไขทรัพยากร Idempotency key จำเป็นเฉพาะสำหรับ POST และ PATCH request เท่านั้น
ควรลองส่ง 4xx error ซ้ำด้วย idempotency key เดิมหรือไม่?
ไม่ควร 4xx error บ่งบอกว่ามีปัญหากับคำขอของคุณ (พารามิเตอร์ไม่ถูกต้อง การยืนยันตัวตนล้มเหลว เป็นต้น) ควรแก้ไขปัญหาและใช้ idempotency key ใหม่สำหรับคำขอที่แก้ไขแล้ว error เดิมจะถูกแคชไว้กับ key เดิม
สามารถใช้ idempotency key เดียวกันข้าม endpoint ต่างๆ ได้หรือ ไม่?
Idempotency key มีขอบเขตแยกตาม endpoint Key เดียวกันที่ใช้กับ endpoint /charges และ /customers จะถูกจัดการแยกกัน อย่างไรก็ตาม แนวปฏิบัติที่ดีที่สุดคือการใช้ key ที่ไม่ซ้ำกันทั่วโลก (UUID) เพื่อหลีกเลี่ยงความสับสน
รูปแบบที่ดีที่สุดสำหรับ idempotency key คืออะไร?
แนะนำให้ใช้ UUID (v4) เนื่องจากรับประกันความไม่ซ้ำกันและมีให้ใช้งานในทุกภาษาโปรแกรม หรือคุณสามารถสร้าง key จาก order/transaction ID ภายในของคุณ (เช่น order-12345) เพื่อให้ติดตามได้ง่ายขึ้น