Idempotency-Key Header Draft を Hono Middleware として実装し理解を深める
この記事は Hono Advent Calendar 2025 の 8 日目の記事です。
昨日の記事は @KEY さんの CloudflareとmdBookとHonoでMarkdownドキュメントをちゃんと限定公開する でした。
プライベートなドキュメントサイトの管理に頭を悩ませるのは結構あるあるだと思っているので、
シュッと OIDC に繋いで限定公開できるとお得ですね。
こういう素朴な道具をシュッと書いてどこにでもデプロイできるのは個人的に Hono が好きな理由の一つです。
さて、本題に入ります。
みなさんは RFC や Internet Draft を読んだことがありますか?
Web 技術の多くは IETF の文書として公開されており、JSONや OAuth 2.0 などの仕様も読むことができます。しかし、実際の開発では必要なところだけ参照することが多く、文章全体の意図や曖昧さに向き合う機会はあまりありません。
では、その RFC や Internet Draft を 実際に自分で実装してみたことはありますか?
技術仕様を実装する際には、文書が示す要件を解釈して具体的な設計に落とし込む必要があるため、アプリケーション開発とは異なる観点が求められます。
この記事では、私が draft-ietf-httpapi-idempotency-key-header-07 を Hono で扱うための Middleware を実装してみた際の経験から、技術仕様を実際に動かしてみるときに考えたことなどを紹介します。
draft-ietf-httpapi-idempotency-key-header-07 とはなにか?
The HTTP Idempotency-Key request header field can be used to make non-idempotent HTTP methods such as POST or PATCH fault-tolerant.
https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-idempotency-key-header-07#abstract
POST や PATCH などの冪等ではない HTTP リクエストを冪等に処理できるようにするための HTTP ヘッダーを提案している Internet Draft です。
具体的には、Client 側は Idempotency-Key というヘッダーに Resource(例: API サーバ) が定める形式の値を設定してリクエストを送信し、 Resource 側は同じ Idempotency-Key を持つリクエストを複数回受け取った場合に Draft で定められた処理を行うことで、冪等にリクエストを処理できるようにします。
これにより、Client 側でネットワークエラーなどによるリクエストの再送が発生した場合でも、 Resource 側で多重に同じ処理をすることを防ぐことができるようになります。
詳しい説明は @ohbayre さんの登壇資料で非常にわかりやすく説明されているので、そちらを参照してください。
なお、この記事で紹介する Hono Middleware を実装する際にも、こちらの資料が非常に参考になりました。ありがとうございます。
実装してみる中で考えたこと
RFC や Internet Draft は、基本的に「誰がどのように振る舞うべきか」を定義しているため、具体的にどのような処理を書くかは実装者が解釈して決める必要があります。
たとえば今回の draft-ietf-httpapi-idempotency-key-header-07 の場合は、以下のような観点を考慮しながら実装を進めました。
- 技術仕様の観点
- ライブラリに落とし込むための観点
- アプリケーションとして運用に載せるための観点
また、迷った際は Implementation Status も見ながら、決済ベンダーなどがどのような振る舞いで実装しているかを参考にしました。
技術仕様の観点
Internet Draft が定める基本的な振る舞いをコードに落とし込むための実装を指します。
ここはあまり迷うポイントはなく、仕様に対するテストを書きながら実装を進めていきます。
-
Idempotency-Keyヘッダーのパース / 妥当性の検証 -
Idempotency-Keyに紐づく Request / Response の保存・取得 - Request の内部状態に応じて適切に処理が分岐すること
- Draft で提案されている Error Scenario に対して適切な Status Code でレスポンスを返すこと
ライブラリに落とし込むための観点
汎用的なライブラリとして実装する場合、「利用者がアプリケーション特性に合わせて挙動をカスタマイズできる」という柔軟性と、「Draft で定められた挙動に準拠させる」という厳密性のバランスを取る必要があります。
特に draft-ietf-httpapi-idempotency-key-header-07 を実装する場合、主に 2.2 ,2.3, 2.4 節で定義されるような Resource Owner が Idempotency-Key ヘッダーに対して定めるポリシーをどう扱うかが重要になります。
このポリシーはユーザーが自由に実装可能であるべきですが、ユーザーの実装を素通ししてしまうと冪等性を担保できない可能性があるため、内部で不適切な実装になっていないか検証するロジックを追加することで対応しました。
他にも以下のような点に注意しました。
- 実行環境に依存する実装は差し替え可能にする
- 具体的には Request / Response を保存するストレージの実装
- ユーザー側に実装を求める interface は primitive なものに限定し、semantics が絡む部分はすべてライブラリ側で実装する
- しくみ上誤った使い方ができないようになっていれば減らせる実装ミスがある
- Idempotency-Key に紐づく Request / Response を表す型は状態に応じて厳密に区別し、一般的な
if文のみで状態を絞り込み可能にする- これはユーザー側から見ると必須ではありませんが、Middleware の実装ミスを静的型検査で検知可能になるので間違った変更をした際に気づきやすくなりました
アプリケーションとして運用に載せるための観点
実アプリケーションの中で運用することを考えると、追加で考慮すべき点があります。
これらはまだ実装できていませんが、興味があるので今後追加で実装したいと考えています。
- atomic な step を定義 / 保存できるようにする
- 複数のマイクロサービスを呼び出すエンドポイントでは途中で処理が失敗するケースがあるので、 atomic step ごとに状態を保存して途中から再開できるようにする必要がある
- ユーザー側に実装を移譲できるようにしたい
- Observability 面の配慮
- ユーザー側で内部処理に対する trace や metrics の計装を認知負荷の低い形で追加する方法を整備する必要がある
- なぜなら、この Draft を運用するなら実際に安全でないリトライを防げているかなどを監視する必要があるため
実際に実装したもの
この Draft を Hono アプリケーションで扱えるように実装したものがこちらです。
単一の Hono Middleware として実装しており、以下のように実装するとエンドポイントに対して自動的に Idempotency-Key ヘッダーを用いた冪等性の担保が適用されます。
import { Hono } from "hono";
import { idempotentRequest } from "hono-idempotent-request";
const app = new Hono()
.use(
"/api",
idempotentRequest({
activationStrategy: "always",
resource: {
satisfiesKeySpec: (key) => {
// Validate Idempotency-Key format.
// Other than this example, you can implement your own logic like UUID validation.
return key.length <= 255;
},
getStorageKey: (idempotencyKey, request) => {
return `${idempotencyKey}:${request.url}`;
},
getFingerprint: async (request: Request) => {
const payload = await request.json();
return JSON.stringify({ user: payload.userId });
},
},
storage: {
adapter: {
get: async (storageKey) => {
// Retrieve a stored request
return yourStorage.get(storageKey);
},
save: async (request) => {
// Save a new request
// The storageKey is available as request.storageKey
await yourStorage.set(request.storageKey, request);
},
update: async (request) => {
// Update an existing request
// The storageKey is available as request.storageKey
await yourStorage.set(request.storageKey, request);
},
},
},
}),
)
.post("/api/payment", async (c) => {
// Your handler logic
return c.json({ status: "success" });
});
まとめ
趣味でいちばん使っているのが Hono なので、今回は Hono 上に draft-ietf-httpapi-idempotency-key-header-07 を実装してみました。Hono は Core API がシンプルなので、技術仕様とのつながりが強い実装も素朴に書けてありがたいです。
Internet Draft から動くものを実装することで、仕様を定める文書と実アプリケーションとの間にあるギャップを解釈して現実的なソフトウェアにするという体験を通じて、仕様に対する理解も深まったと感じています。
みなさんも興味がある RFC や Internet Draft を実際に動かしてみてはいかがでしょうか?
おわりに
Hono Advent Calendar 2025 の 9 日目の記事は @Asahi さんの記事です。
お楽しみに!
Discussion