メインコンテンツへスキップ
バージョン: 最新版

べき等性

重複したcharge、顧客、またはその他のリソースを作成することなく、APIリクエストを安全に再試行します。べき等性キーを使用して信頼性の高い決済統合を構築する方法を学びます。

概要

ネットワークの問題、タイムアウト、サーバーエラーにより、APIリクエストが失敗したり、不明確な結果を返したりする可能性があります。べき等性を使用すると、重複した操作を心配することなく、リクエストを安全に再試行できます。Idempotency-Keyヘッダーを提供することで、Omiseは同じリクエストが複数回送信されても同じ結果を生成することを保証します。

クイックスタート
  • POST/PATCHリクエストにIdempotency-Keyヘッダーを追加します
  • 操作ごとに一意のキーを使用します(UUIDを推奨)
  • 同じキーは同じ結果を返します(24時間キャッシュされます)
  • charge作成と金銭操作に不可欠です
  • ネットワークの問題時に重複した支払いを防ぎます

べき等性とは?

べき等性とは、操作を複数回実行しても同じ結果が得られることを意味します。決済処理では、これは重要です。

べき等性なしの場合

1. chargeリクエストを送信 → ネットワークタイムアウト
2. 成功したか?不明。再試行する?
3. 再試行 → 重複したcharge!顧客に2回請求 💥

べき等性ありの場合

1. べき等性キー付きでchargeを送信 → ネットワークタイムアウト
2. 同じキーで再試行 → 同じ結果が返される
3. 重複したchargeなし ✅

べき等性の仕組み

  1. リクエストを送信する際にIdempotency-Keyヘッダーを付与します
  2. Omiseが処理し、結果を保存します
  3. 再試行する場合、24時間以内に同じキーを使用すると:
    • Omiseはキャッシュされた結果を返します
    • 新しい操作は実行されません
    • 同じレスポンスステータスコードと本文が返されます

フロー例

# 最初のリクエスト(ネットワークタイムアウト)
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_..."
# レスポンス: (タイムアウト - 成功したかどうか不明)

# 同じキーで再試行
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_..."
# レスポンス: 元のchargeを返します(新しいものではありません)

べき等性を使用する場合

常に使用する対象:

Chargeの作成

POST /charges

最も重要 - 重複した支払いを防ぎます

顧客の作成

POST /customers

重複した顧客レコードを防ぎます

返金の作成

POST /charges/:id/refunds

重複した返金を防ぎます

送金の作成

POST /transfers

重複した支払いを防ぎます

受取人の作成

POST /recipients

重複した受取人レコードを防ぎます

すべてのPOSTリクエスト リソースを作成するすべてのPOSTリクエストでべき等性キーを使用する必要があります

PATCHリクエスト 更新はべき等性を使用して安全に再試行できます

不要な対象:

GETリクエスト データの読み取りはすでにべき等です(副作用なし)

DELETEリクエスト 削除は自然にべき等です(2回削除 = 同じ結果)


Idempotency-Keyヘッダー

ヘッダー形式

Idempotency-Key: <unique-string>

キー要件

要件説明
形式最大255文字までの任意の文字列
一意性操作ごとに一意である必要があります
文字英数字とハイフンを推奨
大文字小文字の区別key-1KEY-1
有効期間24時間保存されます

推奨: UUIDを使用

# UUIDv4形式(推奨)
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

なぜUUID?

  • ✅ 一意性が保証されます
  • ✅ 衝突リスクがありません
  • ✅ 標準形式です
  • ✅ すべての言語で利用可能です

実装例

Ruby

require 'omise'
require 'securerandom'

Omise.api_key = ENV['OMISE_SECRET_KEY']

# 一意のべき等性キーを生成
idempotency_key = SecureRandom.uuid

begin
charge = Omise::Charge.create({
amount: 100000,
currency: 'thb',
card: token
}, {
'Idempotency-Key' => idempotency_key
})

puts "Charge created: #{charge.id}"

rescue Omise::Error => e
if e.http_status >= 500 || e.message.include?('timeout')
# 同じキーで安全に再試行
charge = Omise::Charge.create({
amount: 100000,
currency: 'thb',
card: token
}, {
'Idempotency-Key' => idempotency_key # 同じキー!
})
else
raise
end
end

Python

import omise
import uuid

omise.api_secret = os.environ['OMISE_SECRET_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 created: {charge.id}")

except omise.errors.BaseError as e:
if e.http_status >= 500:
# 同じキーで安全に再試行
charge = omise.Charge.create(
amount=100000,
currency='thb',
card=token,
headers={'Idempotency-Key': idempotency_key} # 同じキー!
)
else:
raise

PHP

<?php
require_once 'vendor/autoload.php';

define('OMISE_SECRET_KEY', getenv('OMISE_SECRET_KEY'));

// 一意のべき等性キーを生成
$idempotencyKey = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);

try {
$charge = OmiseCharge::create([
'amount' => 100000,
'currency' => 'thb',
'card' => $token
], [
'Idempotency-Key' => $idempotencyKey
]);

echo "Charge created: " . $charge['id'];

} catch (Exception $e) {
if ($e->getCode() >= 500) {
// 同じキーで再試行
$charge = OmiseCharge::create([
'amount' => 100000,
'currency' => 'thb',
'card' => $token
], [
'Idempotency-Key' => $idempotencyKey // 同じキー!
]);
} else {
throw $e;
}
}

Node.js

const omise = require('omise')({
secretKey: process.env.OMISE_SECRET_KEY
});
const { v4: uuidv4 } = require('uuid');

async function createChargeWithRetry(chargeData, maxRetries = 3) {
// 一意のべき等性キーを生成
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 created: ${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) {
// 同じキーで安全に再試行
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_...'
});

Go

package main

import (
"fmt"
"github.com/google/uuid"
"github.com/omise/omise-go"
"github.com/omise/omise-go/operations"
"time"
)

func createChargeWithRetry(client *omise.Client, amount int64, currency, token string) (*omise.Charge, error) {
// 一意のべき等性キーを生成
idempotencyKey := uuid.New().String()

maxRetries := 3

for attempt := 0; attempt < maxRetries; attempt++ {
charge, err := client.CreateCharge(&operations.CreateCharge{
Amount: amount,
Currency: currency,
Card: token,
Headers: map[string]string{
"Idempotency-Key": idempotencyKey,
},
})

if err == nil {
return charge, nil
}

// 再試行可能かどうかを確認
if omiseErr, ok := err.(*omise.Error); ok {
if omiseErr.StatusCode >= 500 && attempt < maxRetries-1 {
// 指数バックオフで再試行
delay := time.Duration(1<<uint(attempt)) * time.Second
time.Sleep(delay)
continue
}
}

return nil, err
}

return nil, fmt.Errorf("max retries exceeded")
}

func main() {
client, _ := omise.NewClient(
os.Getenv("OMISE_PUBLIC_KEY"),
os.Getenv("OMISE_SECRET_KEY"),
)

charge, err := createChargeWithRetry(client, 100000, "thb", "tokn_test_...")
if err != nil {
log.Fatal(err)
}

fmt.Printf("Charge created: %s\n", charge.ID)
}

べき等性キーの戦略

戦略1: リクエストごとのUUID(推奨)

一意の操作ごとに新しいUUIDを生成します。

# 各chargeに一意のキーを取得
charge1_key = SecureRandom.uuid
charge1 = create_charge(amount: 100_000, key: charge1_key)

charge2_key = SecureRandom.uuid
charge2 = create_charge(amount: 50_000, key: charge2_key)

利点:

  • ✅ シンプルで安全
  • ✅ 衝突リスクなし
  • ✅ すべてのシナリオで機能します

欠点:

  • ⚠️ 後でステータスを確認したい場合はキーを保存する必要があります

戦略2: 注文IDから派生

内部の注文/取引IDに基づいてキーを生成します。

def get_idempotency_key(order_id):
return f"order-{order_id}"

# 注文のchargeを作成
order_id = "ORD-12345"
idempotency_key = get_idempotency_key(order_id)

charge = omise.Charge.create(
amount=100000,
currency='thb',
card=token,
metadata={'order_id': order_id},
headers={'Idempotency-Key': idempotency_key}
)

利点:

  • ✅ 後でキーを再構築しやすい
  • ✅ chargeをシステムにリンクします
  • ✅ 注文がすでに請求されているかどうかを確認できます

欠点:

  • ⚠️ 一意の注文IDが必要です
  • ⚠️ 異なるパラメータで再試行できません

戦略3: リクエストでキーを保存

データベースにキーを保存します。

// データベースに保存
const payment = await db.payments.create({
order_id: 'ORD-12345',
amount: 100000,
idempotency_key: uuidv4(),
status: 'pending'
});

// chargeに保存されたキーを使用
const charge = await omise.charges.create({
amount: payment.amount,
currency: 'thb',
card: token,
headers: {
'Idempotency-Key': payment.idempotency_key
}
});

// ステータスを更新
await db.payments.update(payment.id, {
status: 'completed',
charge_id: charge.id
});

利点:

  • ✅ いつでも再試行可能(キーが保存されています)
  • ✅ 監査証跡
  • ✅ リクエストを関連付けることができます

欠点:

  • ⚠️ より複雑
  • ⚠️ データベースが必要です

戦略4: リクエストパラメータのハッシュ

リクエストの内容からキーを生成します。

<?php
function getIdempotencyKey($chargeParams) {
$content = json_encode($chargeParams);
return hash('sha256', $content);
}

$chargeParams = [
'amount' => 100000,
'currency' => 'thb',
'card' => $token,
'metadata' => ['order_id' => 'ORD-12345']
];

$idempotencyKey = getIdempotencyKey($chargeParams);

$charge = OmiseCharge::create($chargeParams, [
'Idempotency-Key' => $idempotencyKey
]);

利点:

  • ✅ 同じパラメータ = 自動的に同じキー
  • ✅ ストレージ不要
  • ✅ 決定的

欠点:

  • ⚠️ 異なるパラメータ = 異なるキー(同じchargeが必要な場合があります)
  • ⚠️ パラメータの順序に敏感
  • ⚠️ トークンが変更されるとキーが変更されます

キーの有効期限

24時間の有効期間

べき等性キーは、最初の使用から24時間保存されます。

# 1日目、午前10:00 - 最初のリクエスト
curl -H "Idempotency-Key: abc123" ...
# 新しいchargeを作成し、2日目の午前10:00までキーを保存

# 1日目、午前11:00 - 再試行
curl -H "Idempotency-Key: abc123" ...
# 午前10:00からのキャッシュされた結果を返します

# 2日目、午前11:00 - 有効期限後
curl -H "Idempotency-Key: abc123" ...
# キーが期限切れ、新しいchargeを作成(最初のものとは異なります)
キーの有効期限

24時間後、同じキーは新しいリソースを作成します。異なる操作や24時間後にキーを再利用しないでください。

期限切れキーの処理

# ❌ 悪い - 古いキーの再利用
def create_charge_for_order(order_id)
# このキーは注文が古い場合期限切れの可能性があります
key = "order-#{order_id}"

Omise::Charge.create({
amount: 100000,
currency: 'thb',
card: token
}, {
'Idempotency-Key' => key
})
end

# ✅ 良い - 常に新しいキーを使用
def create_charge_for_order(order_id)
# 注文IDとタイムスタンプを組み合わせる
key = "order-#{order_id}-#{Time.now.to_i}"

Omise::Charge.create({
amount: 100000,
currency: 'thb',
card: token,
metadata: { order_id: order_id }
}, {
'Idempotency-Key' => key
})
end

再試行ロジックのベストプラクティス

1. 一時的なエラーのみを再試行

function isRetryable(error) {
// サーバーエラーを再試行
if (error.statusCode >= 500) return true;

// タイムアウトを再試行
if (error.code === 'ETIMEDOUT') return true;
if (error.code === 'ECONNRESET') return true;

// クライアントエラーは再試行しない
if (error.statusCode >= 400 && error.statusCode < 500) return false;

return false;
}

async function chargeWithRetry(chargeData) {
const idempotencyKey = uuidv4();
const maxRetries = 3;

for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await omise.charges.create({
...chargeData,
headers: { 'Idempotency-Key': idempotencyKey }
});

} catch (error) {
if (!isRetryable(error) || attempt === maxRetries - 1) {
throw error;
}

// 指数バックオフ
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}

2. 指数バックオフを使用

import time
import random

def exponential_backoff(attempt, base_delay=1, max_delay=60):
"""ジッターを含む遅延を計算"""
delay = min(base_delay * (2 ** attempt), max_delay)
jitter = random.uniform(0, delay * 0.1) # 10%のジッターを追加
return delay + jitter

def create_charge_with_retry(charge_data, max_attempts=5):
idempotency_key = str(uuid.uuid4())

for attempt in range(max_attempts):
try:
return omise.Charge.create(
**charge_data,
headers={'Idempotency-Key': idempotency_key}
)

except omise.errors.BaseError as e:
is_last_attempt = attempt == max_attempts - 1

if e.http_status < 500 or is_last_attempt:
raise

# 再試行前に待機
delay = exponential_backoff(attempt)
time.sleep(delay)

raise Exception("Max retries exceeded")

3. タイムアウトを設定

require 'timeout'

def create_charge_with_timeout(charge_params, timeout_seconds: 30)
idempotency_key = SecureRandom.uuid

begin
Timeout.timeout(timeout_seconds) do
Omise::Charge.create(
charge_params,
{ 'Idempotency-Key' => idempotency_key }
)
end

rescue Timeout::Error
# タイムアウト - 同じキーで安全に再試行
retry_charge(charge_params, idempotency_key)
end
end

def retry_charge(params, key, max_retries: 3)
max_retries.times do |attempt|
begin
return Omise::Charge.create(
params,
{ 'Idempotency-Key' => key }
)
rescue Omise::Error => e
raise unless e.http_status >= 500
sleep(2 ** attempt)
end
end

raise "Max retries exceeded"
end

4. 再試行の試みをログに記録

const logger = require('./logger');

async function createChargeWithLogging(chargeData) {
const idempotencyKey = uuidv4();
const maxRetries = 3;

logger.info('Creating charge', {
idempotency_key: idempotencyKey,
amount: chargeData.amount,
currency: chargeData.currency
});

for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const charge = await omise.charges.create({
...chargeData,
headers: { 'Idempotency-Key': idempotencyKey }
});

logger.info('Charge created successfully', {
idempotency_key: idempotencyKey,
charge_id: charge.id,
attempt: attempt + 1
});

return charge;

} catch (error) {
logger.error('Charge attempt failed', {
idempotency_key: idempotencyKey,
attempt: attempt + 1,
error_code: error.code,
error_message: error.message,
status_code: error.statusCode
});

const isRetryable = error.statusCode >= 500;
const isLastAttempt = attempt === maxRetries - 1;

if (!isRetryable || isLastAttempt) {
logger.error('Charge failed permanently', {
idempotency_key: idempotencyKey,
total_attempts: attempt + 1
});
throw error;
}

const delay = Math.pow(2, attempt) * 1000;
logger.info('Retrying charge', {
idempotency_key: idempotencyKey,
delay_ms: delay,
next_attempt: attempt + 2
});

await new Promise(resolve => setTimeout(resolve, delay));
}
}
}

高度なパターン

パターン1: 分散システム

複数のサーバーが同じ操作を再試行する可能性がある場合:

# Redisを使用してサーバー間の再試行を調整
require 'redis'

class IdempotentChargeCreator
def initialize(redis_client)
@redis = redis_client
end

def create_charge(order_id, charge_params)
# この注文のロックを取得しようとする
lock_key = "charge_lock:#{order_id}"
idempotency_key = "charge:#{order_id}"

# ロックを設定しようとする(60秒で期限切れ)
locked = @redis.set(lock_key, "locked", nx: true, ex: 60)

unless locked
# 別のサーバーがこれを処理中
return wait_for_charge(order_id)
end

begin
# ロックを取得 - chargeを作成
charge = Omise::Charge.create(
charge_params,
{ 'Idempotency-Key' => idempotency_key }
)

# 結果を保存
@redis.set("charge_result:#{order_id}", charge.to_json, ex: 86400)

charge

ensure
# ロックを解放
@redis.del(lock_key)
end
end

private

def wait_for_charge(order_id)
# 他のサーバーが完了するのを待つ
10.times do
result = @redis.get("charge_result:#{order_id}")
return JSON.parse(result) if result

sleep(0.5)
end

raise "Timeout waiting for charge"
end
end

パターン2: Webhookのべき等性

重複したWebhook配信を処理します。

# Webhookは複数回配信される可能性があります
# イベントIDをべき等性キーとして使用

processed_events = set() # またはデータベース/Redisを使用

def handle_webhook(event_data):
event_id = event_data['id']

# すでに処理されているか確認
if event_id in processed_events:
print(f"Event {event_id} already processed")
return

# イベントを処理
if event_data['key'] == 'charge.complete':
process_successful_charge(event_data['data'])

# 処理済みとしてマーク
processed_events.add(event_id)

# より良い方法: データベースに保存
def handle_webhook_persistent(event_data):
event_id = event_data['id']

# 挿入を試行(event_idに一意制約)
try:
db.events.create({
'event_id': event_id,
'processed_at': datetime.now()
})
except UniqueConstraintError:
# すでに処理済み
return

# イベントを処理
process_event(event_data)

パターン3: バックグラウンドジョブの再試行

べき等なジョブ処理:

// ジョブキューを使用(例: Bull、Sidekiq)
const queue = require('./queue');

queue.process('create-charge', async (job) => {
const { order_id, amount, currency, token } = job.data;

// べき等性キーの一部としてジョブIDを使用
const idempotencyKey = `job-${job.id}-order-${order_id}`;

try {
const charge = await omise.charges.create({
amount,
currency,
card: token,
metadata: { order_id },
headers: { 'Idempotency-Key': idempotencyKey }
});

// 結果を保存
await db.orders.update(order_id, {
charge_id: charge.id,
status: 'charged'
});

return { success: true, charge_id: charge.id };

} catch (error) {
if (error.statusCode >= 500) {
// 再試行可能 - ジョブキューが再試行します
throw error;
} else {
// 再試行不可 - 注文を失敗としてマーク
await db.orders.update(order_id, {
status: 'failed',
error: error.message
});

return { success: false, error: error.message };
}
}
});

// ジョブを追加
queue.add('create-charge', {
order_id: 'ORD-12345',
amount: 100000,
currency: 'thb',
token: 'tokn_test_...'
}, {
attempts: 5,
backoff: {
type: 'exponential',
delay: 2000
}
});

べき等性のテスト

重複リクエストのテスト

# 重複リクエストが同じ結果を返すことをテスト
describe 'Idempotency' do
it 'returns same charge for duplicate requests' do
idempotency_key = SecureRandom.uuid

# 最初のリクエスト
charge1 = Omise::Charge.create({
amount: 100000,
currency: 'thb',
card: token
}, {
'Idempotency-Key' => idempotency_key
})

# 重複リクエスト
charge2 = Omise::Charge.create({
amount: 100000,
currency: 'thb',
card: token
}, {
'Idempotency-Key' => idempotency_key
})

# 同じchargeである必要があります
expect(charge2.id).to eq(charge1.id)
expect(charge2.amount).to eq(charge1.amount)
end

it 'creates new charge with different key' do
# 最初のリクエスト
charge1 = Omise::Charge.create({
amount: 100000,
currency: 'thb',
card: token
}, {
'Idempotency-Key' => SecureRandom.uuid
})

# 異なるキー
charge2 = Omise::Charge.create({
amount: 100000,
currency: 'thb',
card: token
}, {
'Idempotency-Key' => SecureRandom.uuid
})

# 異なるchargeである必要があります
expect(charge2.id).not_to eq(charge1.id)
end
end

ネットワークエラーのシミュレーション

// 再試行ロジックをテスト
const nock = require('nock');

describe('Idempotent Retry', () => {
it('retries on server error', async () => {
const idempotencyKey = uuidv4();

// 最初のリクエストが失敗
nock('https://api.omise.co')
.post('/charges')
.matchHeader('Idempotency-Key', idempotencyKey)
.reply(500, { error: 'Internal Server Error' });

// 再試行が成功
nock('https://api.omise.co')
.post('/charges')
.matchHeader('Idempotency-Key', idempotencyKey)
.reply(200, {
object: 'charge',
id: 'chrg_test_123',
amount: 100000
});

const charge = await createChargeWithRetry({
amount: 100000,
currency: 'thb',
card: 'tokn_test_...'
});

expect(charge.id).toBe('chrg_test_123');
});
});

よくある間違い

❌ してはいけないこと: 操作間でキーを再利用

# ❌ 悪い - 異なる操作に同じキー
idempotency_key = "my-key"

charge1 = omise.Charge.create(
amount=100000,
headers={'Idempotency-Key': idempotency_key}
)

charge2 = omise.Charge.create(
amount=50000, # 異なる金額!
headers={'Idempotency-Key': idempotency_key}
)
# charge2はcharge1の結果を返します!
# ✅ 良い - 操作ごとに一意のキー
charge1 = omise.Charge.create(
amount=100000,
headers={'Idempotency-Key': str(uuid.uuid4())}
)

charge2 = omise.Charge.create(
amount=50000,
headers={'Idempotency-Key': str(uuid.uuid4())}
)

❌ してはいけないこと: キャッシュされたエラーの処理を忘れる

# ❌ 悪い - 元のリクエストが失敗、再試行は同じエラーを返す
idempotency_key = SecureRandom.uuid

begin
Omise::Charge.create({
amount: 100, # 小さすぎる!
currency: 'thb'
}, {
'Idempotency-Key' => idempotency_key
})
rescue Omise::Error => e
# 修正した金額で再試行するが同じキー
Omise::Charge.create({
amount: 100000, # 修正した金額
currency: 'thb'
}, {
'Idempotency-Key' => idempotency_key # 同じキー = キャッシュされたエラー!
})
# 新しいchargeではなく同じエラーを返します
end
# ✅ 良い - 修正したリクエストには新しいキーを使用
begin
Omise::Charge.create({
amount: 100,
currency: 'thb'
}, {
'Idempotency-Key' => SecureRandom.uuid
})
rescue Omise::Error => e
# 修正したリクエストには新しいキーを使用
Omise::Charge.create({
amount: 100000,
currency: 'thb'
}, {
'Idempotency-Key' => SecureRandom.uuid # 新しいキー!
})
end

❌ してはいけないこと: クライアントエラーを再試行

// ❌ 悪い - 無効なリクエストの再試行
async function badRetry() {
const idempotencyKey = uuidv4();

for (let i = 0; i < 3; i++) {
try {
return await omise.charges.create({
amount: 100, // 小さすぎる - 常に失敗します!
currency: 'thb',
headers: { 'Idempotency-Key': idempotencyKey }
});
} catch (error) {
// 再試行しても役に立ちません - リクエストが無効です
continue;
}
}
}

// ✅ 良い - サーバーエラーのみを再試行
async function goodRetry(chargeData) {
const idempotencyKey = uuidv4();

for (let i = 0; i < 3; i++) {
try {
return await omise.charges.create({
...chargeData,
headers: { 'Idempotency-Key': idempotencyKey }
});
} catch (error) {
// サーバーエラーのみを再試行
if (error.statusCode < 500) {
throw error; // クライアントエラーは再試行しない
}
// サーバーエラーの場合は再試行ループを続行
}
}
}

クイックリファレンス

べき等性ヘッダー

Idempotency-Key: <unique-string>

使用する場合

リクエストタイプべき等性を使用?
POST (作成)✅ 常に
PATCH (更新)✅ 推奨
GET (読み取り)❌ 不要
DELETE❌ 不要

キーの有効期間

  • 保存期間: 最初の使用から24時間
  • 期限切れ: 24時間後に新しいリソースを作成

再試行決定ツリー

リクエストが失敗?
├─ はい → エラータイプを確認
│ ├─ 5xx サーバーエラー → 同じキーで再試行
│ ├─ ネットワークタイムアウト → 同じキーで再試行
│ ├─ 4xx クライアントエラー → リクエストを修正、新しいキーを使用
│ └─ その他のエラー → 再試行しない
└─ いいえ → 成功!

実装例

def create_charge_idempotently(params)
key = SecureRandom.uuid

Omise::Charge.create(
params,
{ 'Idempotency-Key' => key }
)
rescue Omise::Error => e
raise unless e.http_status >= 500

# 同じキーで再試行
Omise::Charge.create(
params,
{ 'Idempotency-Key' => key }
)
end

関連リソース


次へ: APIバージョニングについて学習し、バージョン変更を管理し、後方互換性を維持します。