OpenAPI の必須クエリパラメーターが Azure API Management (APIM) でテンプレートパラメーター化され、欠落時に 404 Not Found になってしまう——現場でよく詰まるポイントです。本記事では、なぜそうなるのかの背景から、最小のポリシー実装、API/グローバル共通化、IaC(Bicep/Terraform/ARM)での適用、自動テストまでを一気通貫で解説します。貼り付けて使える XML ポリシー断片も多数掲載します。
APIM 取り込み時に「必須」クエリがテンプレート化される現象の理解
OpenAPI では parameters[].in: query に対して required: true を付けることで、クライアントに「必須」であることを伝えます。一方 APIM は、インポート時のルーティング最適化として、必須クエリをテンプレートパラメーター(ルート解決対象)へ昇格させることがあります。これにより該当クエリが存在しないリクエストは「ルートにマッチしない」と判定され、APIM からは 404 が返ります。本来、クライアントは 400(Bad Request)を期待しているのに、ゲートウェイで 404 が返るため実装/監視の両面で不整合が生じます。
最小再現例(OpenAPI v3, 抜粋)
paths:
/items:
get:
parameters:
- name: id
in: query
required: true
schema:
type: string
responses:
'200':
description: OK
上記を APIM にインポートすると、id が欠落した GET /items に対して APIM が 404 を返し得ます。これが本記事で扱う現象です。
解決の基本方針:Policy で 404 → 400 を明示的に補正する
APIM のルーティング挙動自体はコントロールできないため、ポリシー(Policy)で入力検証を行い、要件を満たさない場合は 400 を返すのが堅実かつ一般的な対処です。Operation 単位/API 単位/グローバル(全 API)単位のいずれにも適用できます。
個別 Operation での最小実装
Azure ポータルでも IaC でも設定方法は同じです。対象 Operation の <inbound> に次の断片を追加します。
<choose>
<when condition="@(
string.IsNullOrEmpty(
context.Request.OriginalUrl.Query.GetValueOrDefault("id")
)
)">
<return-response>
<set-status code="400" reason="Bad Request" />
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>{
"error": "必須クエリ 'id' がありません。"
}</set-body>
</return-response>
</when>
</choose>
idを実際の必須クエリ名に置き換えてください。- 複数ある場合は
<when>ブロックを複数追加するか、後述の「共通化」案を使うと保守が容易です。
戻り値を 400 にする意義
- クライアント実装の一貫性:バリデーションエラーは 400 として扱うのが一般的で、クライアント側のリトライ/エラーハンドリングが正しく機能します。
- 監視/分析の正確性:404 はルーティング/URL ミス、400 は入力エラーとしてメトリックを分離でき、SLO/SLA 分析が鮮明になります。
多数のパラメーターをまとめて検証する(API/グローバル共通化)
API 全体や複数 Operation に跨って必須クエリが多数ある場合、共通化して 保守コストを劇的に下げるのがポイントです。APIM Policy は C# ベースの式を使えるため、配列で一覧を持ち、欠落している最初の要素を検出するロジックを 1 箇所に集約できます。
API スコープでの共通ポリシー例
<inbound>
<base />
string.IsNullOrEmpty(context.Request.OriginalUrl.Query.GetValueOrDefault(n))
);
}" />
application/json
{
"error": "必須クエリが不足しています。",
"missing": "@((string)context.Variables["missingQueryParam"])"
}
Operation ごとに必須項目が異なるなら、次のようにルートで分岐します。
<set-variable name="route" value="@((string)context.Request.OriginalUrl.Path)" />
<choose>
<when condition="@(((string)context.Variables["route"]).StartsWith("/items"))">
<set-variable name="requiredQueryParams" value="@(new [] { "id" })" />
</when>
<otherwise>
<set-variable name="requiredQueryParams" value="@(new string[0])" />
</otherwise>
</choose>
Policy フラグメントでの再利用
APIM の「ポリシーフラグメント」を使うと、検証ロジックを 1 箇所に定義して複数 API から参照できます。
<!-- フラグメント: require-query-params -->
<set-variable name="missingQueryParam" value="@{
var required = (string[])context.Variables.GetValueOrDefault("requiredQueryParams") ?? new string[0];
return required.FirstOrDefault(n => string.IsNullOrEmpty(context.Request.OriginalUrl.Query.GetValueOrDefault(n)));
}" />
application/json
{
"error":"必須クエリが不足しています",
"missing":"@((string)context.Variables["missingQueryParam"])"
}
呼び出し側(API/Operation)では、必須一覧だけ与えてフラグメントを <include-fragment> します。
<set-variable name="requiredQueryParams" value="@(new [] { "id", "tenantId" })" />
<include-fragment fragment-id="require-query-params" />
入力値の型・範囲チェックもまとめてやる
「存在チェック」だけでなく、型や範囲もゲートで早期に弾きましょう。例として、page は整数で 1 以上、size は 1〜100 の範囲に制約します。
<set-variable name="pageStr" value="@(context.Request.OriginalUrl.Query.GetValueOrDefault("page"))" />
<set-variable name="sizeStr" value="@(context.Request.OriginalUrl.Query.GetValueOrDefault("size"))" />
= 1;
}" />
= 1 && v <= 100);
}" />
application/json
{"error":"page は 1 以上の整数で指定してください"}
application/json
{"error":"size は 1〜100 の整数で指定してください"}
IaC での適用:Bicep / Terraform / ARM
Bicep(Operation ポリシー)
resource op 'Microsoft.ApiManagement/service/apis/operations@2022-08-01' existing = {
name: '${apim.name}/${api.name}/get-items'
}
resource opPolicy 'Microsoft.ApiManagement/service/apis/operations/policies@2022-08-01' = {
name: '${op.name}/policy'
parent: op
properties: {
format: 'rawxml'
value: '''
application/json
{"error":"必須クエリ 'id' がありません"}
'''
}
}
Terraform(Operation ポリシー)
resource "azurerm_api_management_api_operation_policy" "items_get_policy" {
api_management_name = azurerm_api_management.apim.name
resource_group_name = azurerm_resource_group.rg.name
api_name = azurerm_api_management_api.api.name
operation_id = azurerm_api_management_api_operation.items_get.operation_id
xml_content = <<POLICY
<policies>
<inbound>
<base />
<choose>
<when condition="@(
string.IsNullOrEmpty(
context.Request.OriginalUrl.Query.GetValueOrDefault("id")
)
)">
<return-response>
<set-status code="400" reason="Bad Request" />
<set-header name="Content-Type" exists-action="override"><value>application/json</value></set-header>
<set-body>{"error":"必須クエリ 'id' がありません"}</set-body>
</return-response>
</when>
</choose>
</inbound>
<backend />
<outbound />
<on-error />
</policies>
POLICY
}
ARM(API スコープ)
{
"type": "Microsoft.ApiManagement/service/apis/policies",
"apiVersion": "2022-08-01",
"name": "[concat(parameters('apimName'), '/', parameters('apiName'), '/policy')]",
"properties": {
"format": "rawxml",
"value": "<policies><inbound><base /><set-variable name='requiredQueryParams' value='@(new [] { "id" })' /><include-fragment fragment-id='require-query-params' /></inbound><backend /><outbound /><on-error /></policies>"
}
}
パイプラインに組み込むヒント
- OpenAPI インポート直後に「検証ポリシー差し込み」ステップを実行(フラグメントを事前発行 → API/Operation に適用)。
- 必須クエリ一覧は CI の変数ファイル(JSON/YAML)化してサービス/バージョンごとに管理。
エラーレスポンス設計(例)
クライアント開発者がデバッグしやすいよう、エラーコードと具体的な欠落項目を返しましょう。
{
"error": {
"code": "MissingQueryParameter",
"message": "必須クエリが不足しています。",
"details": [
{ "name": "id", "reason": "required" }
],
"correlationId": "{@(context.RequestId)}"
}
}
APIM のポリシー式で context.RequestId を挿入しておくと、ゲートウェイの診断ログとの突合が容易になります。
挙動比較:放置 vs Policy 補正
| 観点 | 放置(テンプレート化のまま) | Policy で 400 に補正(推奨) |
|---|---|---|
| 欠落リクエストのステータス | 404 Not Found | 400 Bad Request |
| クライアントの再送ロジック | 404 と誤認しルーティング調査に工数 | 入力エラーとして即時修正に誘導 |
| 監視/アラートの精度 | URL ミスと入力ミスが混在 | 原因別に切り分け可能 |
| 運用コスト | 問い合わせ増加・トリアージ負荷 | エラー内容が明確で自己解決率が上がる |
よくある落とし穴と回避策
- 「required を外す」対処は非推奨:OpenAPI をアプリから自動生成しているならスキーマ真実性を下げます。APIM で補正しましょう。
- Operation 毎にポリシーを複製しすぎる:フラグメント化や API スコープでの共通化が効果的。必須一覧だけ差し替える構造に。
- 型/範囲の検証漏れ:存在チェックだけでなく整数/列挙/正規表現もゲートで早期に弾くとバックエンド負荷を下げられます。
- Content-Type 未指定:JSON を返すなら
Content-Type: application/jsonを必ず明示。 - 多言語対応:運用チーム向けの英語メッセージと、クライアント向けの日本語メッセージを切り替えたい場合は Named Value やヘッダーで分岐。
リクエスト例とテストレシピ
cURL
# 欠落: id なし(期待 = 400)
curl -i "https://<gateway>/items"
# 正常: id あり(期待 = 200)
curl -i "https:///items?id=abc123"
Postman/Newman での自動化ポイント
- シナリオ 1:必須クエリ欠落 →
pm.response.code === 400をアサート。 - シナリオ 2:異常型(例:
page=a) → 400 と精緻なメッセージをアサート。 - シナリオ 3:境界値(例:
size=0/size=101)→ 400。
高度な設計:Operation ごとに必須クエリを宣言的に渡す
「宣言(必須一覧)+共通バリデーション」の 2 層に分けると、規模が増えても崩れません。下記は各 Operation の頭で変数だけ設定し、直後にフラグメントを呼ぶ構成です。
<inbound>
<base />
<!-- この Operation は id, tenantId が必須 -->
<set-variable name="requiredQueryParams" value="@(new [] { "id", "tenantId" })" />
<include-fragment fragment-id="require-query-params" />
新規 Operation を追加しても、宣言するだけで同じ品質のエラーハンドリングを自動適用できます。
JSON スキーマの検証と組み合わせる
ボディが JSON の場合は ボディはスキーマ検証、クエリは本記事の方式という二段構えにしましょう。バリデーションのレイヤが明確になり、バグ探索が容易です。
監視・診断のコツ
- 失敗率の分離:ゲートで 400 に倒すと、バックエンド 4xx/5xx から独立して可視化できます。
- 相関 ID:
context.RequestIdをエラー JSON に含め、APIM のログ/トレースと突合。 - カスタムヘッダー:
X-Error-Codeを付与するとフロントの可観測性が向上。
<set-header name="X-Error-Code" exists-action="override">
<value>MISSING_QUERY_ID</value>
</set-header>
セキュリティ観点の注意
- エラー詳細に秘匿情報を含めない(例:内部キー名、実 DB カラム名など)。
- 列挙型の値域は、存在チェックだけでなくホワイトリストで検証。
- リクエストサイズ上限やレート制限と併用し、DoS 耐性を確保。
アンチパターン/代案比較
| アプローチ | メリット | デメリット | 適用可否 |
|---|---|---|---|
| Policy で 400 化(推奨) | 低コスト・即効性・既存スキーマを保てる | 必須一覧のメンテが必要(共通化で軽減) | ◎ |
OpenAPI の required を外す | APIM の 404 問題は回避 | スキーマの正確性が落ち、SDK/Doc に悪影響 | ×(非推奨) |
| ルート書き換え(rewrite-uri)で回避 | 一見 404 を避けられる | 欠落を見逃してバックエンドに到達しがち | △(限定的) |
| 将来の拡張/設定に期待 | 保守性は高い可能性 | 現時点の実務課題は解けない | — |
FAQ
Q. OpenAPI v2(Swagger)でも起きますか?
取り込み時のルーティング最適化は仕様依存で、クエリ必須 → ルート要素化という本質は v2/v3 いずれでも起こり得ます。発生する前提でポリシー側で担保する方が安全です。
Q. Developer Portal の「試してみる」で 404 になります。
同じ原因です。Portal の動作は APIM 経由なので、欠落時に 404 が出る場合はポリシー補正を入れて 400 に揃えましょう。
Q. 必須だが空文字は可、という要件は?
「存在チェック」は pass、「空は可」なら IsNullOrEmpty ではなく GetValueOrDefault(..., string.Empty) 等で null だけを弾くように条件を書き分けます(空文字可の基準を合意してから実装してください)。
Q. バックエンドでも検証すべき?
はい。ゲートで早期弾き+アプリで最終バリデーションの二重化が堅牢です。ゲートは軽量な否定判定、アプリはドメインロジックの精密判定を担当します。
まとめ:運用に強いチェックリスト
- 必須クエリは APIM で 404 になり得ると理解する。
- Operation から開始し、API/フラグメントへ共通化して保守性を高める。
- 存在だけでなく型/範囲/列挙も検証、明確な 400 を返す。
- エラー JSON に相関 ID・エラーコードを付けて可観測性を上げる。
- CI/CD に「ポリシー差し込み」ステップを入れて人手の揺れを無くす。
以上で、OpenAPI の「必須」クエリが APIM でテンプレートパラメーター化されることによる 404 を、一貫して 400 に正規化する実装指針を網羅しました。掲載のポリシー断片をそのまま貼り付けつつ、フラグメント化と IaC を併用することで、規模が大きくなっても運用コストを最小化できます。
付録:複数パラメーターの宣言+検証を 1 ブロックに纏めた完全版
<policies>
<inbound>
<base />
<!-- Operation ごとの宣言。ここだけ編集すればよい -->
<set-variable name="requiredQueryParams" value="@(new [] { "id", "tenantId", "locale" })" />
<!-- 存在チェック -->
<set-variable name="missingQueryParam" value="@{
var req = (string[])context.Variables["requiredQueryParams"];
return req.FirstOrDefault(n => string.IsNullOrEmpty(context.Request.OriginalUrl.Query.GetValueOrDefault(n)));
}" />
<choose>
<when condition="@(!string.IsNullOrEmpty((string)context.Variables["missingQueryParam"]))">
<return-response>
<set-status code="400" reason="Bad Request" />
<set-header name="Content-Type" exists-action="override"><value>application/json</value></set-header>
<set-body>{
"error":"必須クエリが不足しています",
"missing":"@((string)context.Variables["missingQueryParam"])",
"correlationId":"@((string)context.RequestId)"
}</set-body>
</return-response>
</when>
</choose>
<!-- 型/範囲チェックの例: locale は ja|en のみ -->
<set-variable name="locale" value="@(context.Request.OriginalUrl.Query.GetValueOrDefault("locale"))" />
<choose>
<when condition="@{
var lc = (string)context.Variables["locale"];
return !(lc == "ja" || lc == "en");
}">
<return-response>
<set-status code="400" reason="Bad Request" />
<set-header name="Content-Type" exists-action="override"><value>application/json</value></set-header>
<set-body>{"error":"locale は ja / en のいずれか"}</set-body>
</return-response>
</when>
</choose>
付録:運用チェックリスト(貼って使える)
- OpenAPI の必須クエリ一覧を棚卸し済みか。
- API/Operation で requiredQueryParams を宣言し、フラグメントを適用しているか。
- エラー JSON に correlationId と X-Error-Code を含めているか。
- 整数・範囲・列挙の型検証をゲートで実施しているか。
- CI/CD の「ポリシー差し込み」ステップが安定稼働しているか。
- 404/400 のメトリックを分離し、ダッシュボードで可視化しているか。

コメント