
Contentful Appから外部APIを呼ぶと遅い?App Action経由の約3秒を署名付きリクエストで約1秒にした
ベルリンオフィスの小西です。
DevelopersIO の記事は Contentful で管理・執筆しています。記事タグ欄にはカスタムプラグインとしてタグ推薦機能を追加しています。

これまでは Claude Haiku 4.5(Bedrock + Lambda)で出していたのですが、先日 Cloudflare から出た決定モデル Clef に切り替えました。Cloudflare Workers のAPIとして作り直し、サンプル記事数百件で比べたところ、精度も速度(約3秒 → 約0.7秒)も良くなったのでデプロイ。ところがCMS画面でボタンを押すと、明らかに待たされる感じがありました。測ってみると約3秒。API 単体では0.7秒なのに。
この記事では3秒かかっていた理由と、署名付きリクエスト(App Signed Requests)で解決できた話を書きます。
Custom App の全体像は過去記事をどうぞ。
変更前の構成: App Action + Functions
当初は App Action + Contentful Functions 経由で外部APIを呼んでいました。外部APIの認証情報をサーバー側に置きたかったためです。
流れ:
- ブラウザ(App のフロントエンド)で
sdk.cma.appActionCall.createWithResponseを呼ぶ - Contentful が App Action の呼び出しを作成し、Function を起動する
- Function が外部APIを呼ぶ
- ブラウザは Function の結果ができるまで待つ
どこで時間がかかっているか
合計約3.2秒のうち、7割強(約2.3秒)が Contentful 側でした。
ブラウザ・Function・外部API の3か所で時間を測った結果です(ベルリンから Chrome で5回計測した中央値)。
| 合計 | Function | 外部API内の処理 | Contentful側(合計 − Function) |
|---|---|---|---|
| 3,186ms | 856ms | 742ms | 2,330ms |
外部API内の処理は約0.7秒、Function と外部API の間の通信は約0.1秒で、Function 自体はほぼ時間を使っていません。
なお「Contentful側」は合計から Function の時間を引いた残りなので、ブラウザと Contentful API の間の通信時間も含みます。また Function 経由のとき、外部API(Worker)は米国東部(IAD)で動いていました(ログで確認)。Function が米国で実行されているとみられます。
時間がかかる仕組み
原因は、createWithResponse が既定で結果を2秒間隔で取りにいくことです。App SDK の sdk.cma に同梱されている contentful-management のコードは、次のように動いています。
- App Action の呼び出しを作成する(
POST /spaces/{space}/environments/{env}/app_installations/{app}/actions/{action}/calls)。この時点で返るのは呼び出しの ID だけ - 結果を取得する(
GET .../actions/{action}/calls/{callId}) - 結果がまだなければ 2秒待って 取り直す(既定値。
retryIntervalを渡さなければ 2000ms)
Function が約0.9秒で終わっても、最初の取得では結果がまだなく、2秒待って取り直すので約3秒になります。間に合わなければもう2秒。5回中1回あった5.5秒はこれだと思われます。
既定値のままなら、待ち時間は外部API の速さではなく「2秒刻み」で決まるので、外部API が0.7秒でも0.3秒でも体感はほぼ変わりません。Clef で速くした分は相殺されていました。
呼び出し時に retryInterval を渡せば間隔は短くできそうですが、今回は試していません。短くしても、呼び出しの作成・結果の取得の往復と Function の起動は残ります。
時間がかかっても問題ない非同期処理向けの作りで、ボタンを押してすぐ結果が欲しい画面には合わない印象です。
解決: 署名付きリクエストでブラウザから直接呼ぶ
App Signed Requests を使うと、App Action・Function・結果の取り直しを通らず、ブラウザ → 外部API の1往復で済みます。API トークンをブラウザに置く必要もありません。
App のフロントエンドが Contentful に「このリクエストに署名して」と頼み、署名ヘッダーをもらう仕組みです。外部API 側は App 定義ごとの signing secret で署名を検証し、正しい App からのリクエストだけを受け付けます。
フロントエンド側
sdk.cma.appSignedRequest.create で署名ヘッダー(additionalHeaders)をもらい、そのまま付けて fetch します。
const requestBody = JSON.stringify({ text, entryId: sdk.ids.entry });
const request = {
method: 'POST' as const,
path: '/suggest',
headers: { 'Content-Type': 'application/json' },
body: requestBody,
};
const { additionalHeaders } = await sdk.cma.appSignedRequest.create(
{ appDefinitionId: sdk.ids.app! },
request
);
const res = await fetch(`${apiUrl}/suggest`, {
method: 'POST',
headers: { ...request.headers, ...additionalHeaders },
body: requestBody,
});
署名した本文(requestBody)と送る本文は同じ文字列にします。JSON を作り直すと、キーの順番などが変わって署名が合わなくなります。
署名用の秘密鍵(signing secret)
App 定義ごとに1つ、64文字の秘密鍵を設定します。管理画面(Security タブ)か、CMA の PUT /organizations/{org}/app_definitions/{id}/signing_secret で作れます。取得しても値は伏せ字なので、作った時点で外部API 側のシークレットにも置きます。
[画像: App 定義の Security タブ(Request verification の設定画面)]
外部API 側でやること
外部API 側では、次の4点に対応しました。
- 署名の検証: メソッド・パス・署名対象ヘッダー・本文から HMAC-SHA256 で署名を作り直して比べます。有効期限は署名から30秒(公式ライブラリの既定)。公式の
verifyRequest(@contentful/node-apps-toolkit)は Node 前提なので、Cloudflare Workers では Web Crypto で同じ手順を実装しました。公式と同じく、定数時間での比較(タイミング攻撃対策)と期限チェック(リプレイ対策)も入れています - パラメータは本文に入れる: クエリ文字列は署名時のエスケープを避けるため本文に入れました
- CORS: ブラウザから直接呼ぶのでプリフライトが飛びます。
Access-Control-Allow-Headersにx-contentful-signatureなどの署名関連ヘッダーが必要です - スペースで制限: 署名対象の
x-contentful-space-idを見て、決めたスペース以外は 401 で弾きます
なお、署名が保証するのは「そのスペースの App 経由のリクエストである」ことまでです。スペースの編集者なら、開発者ツールから正規の署名付きで API を呼べる点には注意が必要です。
結果
約3.2秒 → 約1.1秒(中央値、−65%)になりました。体感もけっこう違います。
| App Action + Functions | 署名付きリクエストで直接 | |
|---|---|---|
| 中央値 | 3,186ms | 1,101ms(−65%) |
| 平均 | 3,606ms | 1,275ms |
| 最大 | 5,547ms | 1,727ms |
変更後はほぼ外部APIの処理時間どおりで、ぶれも小さくなりました(最大 5.5秒 → 1.7秒)。内訳(中央値)は、署名の取得 312ms、ブラウザと外部API の間の通信 109ms、外部API内の処理 700ms です。
ただ、署名の取得も Contentful への1往復なので、ときどき遅くなります(0.88秒の回あり)。
どちらを選ぶか
App Action + Functions が悪いわけではなく、向き不向きの話かと思ってます。
| App Action + Functions | 署名付きリクエストで直接 | |
|---|---|---|
| 速さ | 既定では結果の取得が2秒間隔なので、2秒刻みで遅くなる | 外部API の時間 + 署名の取得(約0.3秒) |
| 秘密情報 | Function 側に置ける | 外部API 側で署名を検証するので、ブラウザに秘密情報が要らない |
| 外部API 側の作業 | 不要 | CORS の設定と署名の検証が必要 |
| 向いている処理 | 時間のかかる処理、外部API を変更できない場合 | 応答の速さが大事な処理、外部API を自分で持っている場合 |
外部API に手を入れられるなら、UI から呼ぶ処理は署名付きリクエストのほうが素直です。
補足: インストール設定に秘密情報を置かない
App のインストール設定(sdk.parameters.installation)は、通常の型だとブラウザから読めます。API トークンや HMAC の秘密鍵を置くと、エントリを編集できる人なら開発者ツールで見られます。
秘密情報を置くなら、App 定義で Secret 型 のインストールパラメータを使います。Secret 型の生の値は Functions などのバックエンド側でしか読めず、App SDK や CMA(個人アクセストークン)からは伏せ字になります。署名付きリクエストならトークン自体が要りません。
さいごに
署名付きリクエスト自体は前からある機能で、公式ドキュメントにも App のフロントエンドから自前のバックエンドを呼ぶ用途として載っています。App Action + Functions のほうが目に付きやすいですが、UI から呼ぶならこちらのほうが素直でした。Contentful App で「なんか遅いな」と思ったら、一度経路を疑ってみるといいかもしれません。










