Skip to content
Published on

HTTP QUERY メソッド — RESTの長年のジレンマを解く新しい動詞

Authors

はじめに

最近、GeekNews(news.hada.io)やHacker NewsでHTTP QUERYメソッドに関する記事が大きな話題になりました。特にkreya.appブログの「HTTP QUERY method」という記事がGeekNewsの人気記事に上がり、開発者の間で「ついに検索APIをきちんと設計できる動詞ができるのか」という反応が広がりました。

RESTful APIを一度でも真剣に設計したことがある人なら、誰もが直面したジレンマがあります。複雑な検索条件をどうやってサーバーに送るかという問題です。フィルタが単純ならGETのクエリ文字列で十分ですが、ネストしたフィルタにソートやページネーション、集計条件が混ざった複雑な検索となると話が変わります。

GETを使えばリクエストボディを送れず、URL長の制限にぶつかります。POSTを使えばボディは送れますが、「検索」という行為に対して意味論がまったく合いません。POSTは安全(safe)でも、冪等(idempotent)でも、キャッシュ可能(cacheable)でもないからです。

この記事では、この古いジレンマの本質を整理し、IETFが標準化を進めているHTTP QUERYメソッドがどのようにこの問題を解決するのか、そして実務で今何を考慮すべきかを深く見ていきます。


1. 問題の本質 — GETとPOSTの間の隙間

1.1 GETの限界

GETはHTTPの最も基本的な動詞であり、リソースを取得するために使われます。GETの中心的な性質は次のとおりです。

  • 安全(Safe): サーバーの状態を変更しません。
  • 冪等(Idempotent): 何度呼び出しても結果が同じです。
  • キャッシュ可能(Cacheable): レスポンスをキャッシュできます。

問題は、GETが意味論的にボディを持たないという点です。RFC 9110は、GETリクエストにボディを送ることについて「定義された意味論はなく、一部の実装はこれを拒否することがある」と明記しています。つまりGETのボディは事実上使えません。

そのため複雑な検索条件はすべてURLのクエリ文字列に詰め込まなければなりません。ここで三つの現実的な問題が発生します。

問題領域具体的な症状
URL長の制限ブラウザやプロキシ、サーバーごとに上限が異なり、通常2KBから8KB程度
エンコード負荷ネスト構造をクエリ文字列で表現すると直列化ルールが煩雑になる
ログ露出検索語と条件がアクセスログ、プロキシログ、履歴に残る

特にURL長の制限は標準で定められた値ではなく実装依存です。一部の古いプロキシやサーバーはより短い上限を持つことがあり、長い検索条件は予測不能に切り詰められたり拒否されたりします。

クエリ文字列でネスト構造を表現する例を見てみましょう。

GET /products?filter[category]=shoes&filter[price][gte]=100&filter[price][lte]=200&sort[]=-rating&sort[]=price&page[number]=2&page[size]=20

この方式は標準がなく、フレームワークごとにパースルールが異なります。同じ意図をJSONで表現するとはるかに明確です。

{
  "filter": {
    "category": "shoes",
    "price": { "gte": 100, "lte": 200 }
  },
  "sort": ["-rating", "price"],
  "page": { "number": 2, "size": 20 }
}

JSONボディの方が明らかに読みやすく構造的です。しかしGETではこのボディを送れません。

1.2 POSTの意味論的なずれ

そこでほとんどの開発者はPOSTに逃げます。POSTはボディを自由に送れるので、上のJSONをそのまま載せて送ればよいのです。

POST /products/search HTTP/1.1
Host: api.example.com
Content-Type: application/json

{
  "filter": { "category": "shoes", "price": { "gte": 100, "lte": 200 } },
  "sort": ["-rating", "price"],
  "page": { "number": 2, "size": 20 }
}

動作はします。しかし意味論的にPOSTは検索にまったく合わない動詞です。POSTの標準的な性質を見てみましょう。

性質GETPOST検索に必要か
安全性ありなし必要
冪等性ありなし必要
キャッシュありなし有用

検索は本質的に読み取り操作です。サーバーの状態を変えず(安全)、何度呼び出しても同じ結果を返し(冪等)、同じ条件ならレスポンスをキャッシュできる(キャッシュ可能)べきです。しかしPOSTはこの三つをいずれも保証しません。

POSTを検索に使うと次のような副作用が生じます。

  • 中間キャッシュ(CDN、リバースプロキシ)がレスポンスをキャッシュできません。
  • クライアントライブラリやプロキシが安全に再試行(retry)できません。POSTの再試行は副作用を起こしかねないと仮定されるためです。
  • モニタリングやAPIゲートウェイがこのリクエストを「書き込み」に分類し、読み取り専用ポリシー(例えば読み取りレプリカへのルーティング)を適用できません。

要するに、GETにはボディがなく、POSTは意味が間違っています。この隙間こそ、QUERYメソッドが埋めようとする場所です。


2. HTTP QUERYメソッドとは何か

2.1 一行の定義

HTTP QUERYメソッドは、リクエストボディを持つことができ、かつ安全で冪等な新しいHTTP動詞です。簡単に言えば「ボディを送れるGET」です。

IETFのHTTPワーキンググループ(httpbis)が「HTTP QUERY Method」というインターネットドラフト(draft-ietf-httpbis-safe-method-w-body)として標準化を進めています。ドラフト名に込められた「safe-method-with-body(ボディを持つ安全なメソッド)」という表現が、この動詞の本質を正確に要約しています。

2.2 中心的な性質

QUERYメソッドの意味論は次の三つに整理されます。

性質QUERY意味
安全性ありサーバーの状態を変更しない読み取り操作
冪等性あり何度呼び出しても同一の結果を保証
ボディ許可ありリクエストボディに構造化された検索条件を入れられる

これに加えて、レスポンスは適切な条件が揃えばキャッシュできます。この点が核心です。QUERYはボディを持ちながらもキャッシュ可能性を捨てません。

三つの動詞を一目で比較すると次のようになります。

項目GETPOSTQUERY
リクエストボディ事実上不可可能可能
安全性ありなしあり
冪等性ありなしあり
キャッシュありなしあり(条件付き)
検索適合性ボディ制約意味がずれる意味が正確に合う

2.3 リクエストとレスポンスの形

QUERYリクエストはGETのように安全ですが、POSTのようにボディを載せて送ります。先ほどの商品検索をQUERYで表現すると次のようになります。

QUERY /products HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json

{
  "filter": {
    "category": "shoes",
    "price": { "gte": 100, "lte": 200 }
  },
  "sort": ["-rating", "price"],
  "page": { "number": 2, "size": 20 }
}

サーバーは一般的な検索結果を返します。

HTTP/1.1 200 OK
Content-Type: application/json

{
  "total": 142,
  "results": [
    { "id": "p-1001", "name": "Trail Runner", "price": 159 },
    { "id": "p-1002", "name": "City Walker", "price": 129 }
  ]
}

GETと比べると動詞が変わっただけで意味はそのまま「取得」です。POSTと比べるとボディは同じように送りますが、安全性と冪等性という意味論的保証が加わります。


3. QUERYレスポンスのキャッシュ可能性

3.1 キャッシュキーの問題

GETレスポンスをキャッシュするとき、キャッシュキーは通常URLです。同じURLなら同じレスポンスという前提です。しかしQUERYでは検索条件がボディに入っています。URLは同じでボディだけが異なる二つのリクエストが、異なる結果を返しうるということです。

したがってQUERYレスポンスをキャッシュするにはボディもキャッシュキーに含める必要があります。HTTP QUERYドラフトはそのためのメカニズムを議論しています。中心的なアイデアは、サーバーがレスポンスで正規化された(canonical)キャッシュキー情報を提供し、中間キャッシュがそれを活用するというものです。

3.2 Content-Locationによるキャッシュ処理

一つのアプローチは、サーバーがレスポンスに Content-Location ヘッダーで「このクエリに対応する正規URL」を知らせる方式です。そうすれば同一のクエリに対してはそのURLをキャッシュキーとして使えます。

QUERY /products HTTP/1.1
Host: api.example.com
Content-Type: application/json

{ "filter": { "category": "shoes" }, "page": { "size": 20 } }

レスポンスで正規の位置を提示します。

HTTP/1.1 200 OK
Content-Type: application/json
Content-Location: /products?category=shoes&size=20
Cache-Control: max-age=60

{ "total": 88, "results": [] }

こうすればボディベースの検索の表現力と、GETベースのキャッシュの利点を同時に得られます。ただしこの部分はドラフトで依然として議論が進行中の領域なので、実装時には最新ドラフトの推奨に従う必要があります。

3.3 キャッシュ動作の比較

シナリオGETPOSTQUERY
ブラウザキャッシュデフォルト動作キャッシュされない可能(実装が必要)
CDN / リバースプロキシ広範に対応通常は非対応段階的な対応が見込まれる
キャッシュキーURLキャッシュしないURLとボディ

4. これまでの回避策

QUERYが標準化される前まで、業界はさまざまな回避策でこの問題に対処してきました。それぞれの長所と短所を見ると、なぜQUERYが必要なのかがより明確になります。

4.1 検索専用のPOSTエンドポイント

最も一般的な方式です。検索のための別のPOSTエンドポイントを作るというものです。Elasticsearchの検索APIが代表的な例です。

POST /products/_search HTTP/1.1
Host: search.example.com
Content-Type: application/json

{
  "query": {
    "bool": {
      "must": [{ "match": { "category": "shoes" } }],
      "filter": [{ "range": { "price": { "gte": 100, "lte": 200 } } }]
    }
  },
  "sort": [{ "rating": "desc" }],
  "from": 20,
  "size": 20
}

ElasticsearchはGETとPOSTの両方を許可しますが、ボディを安全に送るために事実上POSTが標準的な慣行になりました。動作には問題ありませんが、先ほど見た意味論的な限界をそのまま抱えます。検索が「書き込み」に分類され、キャッシュされません。

URLの末尾に付く _search のような動詞型のパス(action path)も、RESTfulな設計原則と食い違います。RESTは名詞型のリソースを志向しますが、_search は明らかに動詞だからです。

4.2 巨大なGETクエリ文字列

もう一つの方式は、すべての条件をGETのクエリ文字列に詰め込むことです。単純な検索ではうまく動作しますが、条件が複雑になるほどURLが制御不能なほど長くなります。

GET /products?q=shoes&min=100&max=200&sort=-rating,price&fields=id,name,price&include=reviews&filter.brand.in=nike,adidas&page=2&size=20

URL長の制限、エンコード地獄、ログ露出の問題がすべて重なります。さらにネスト構造やOR条件のような複雑な論理をクエリ文字列で表現する標準がなく、互換性の問題が絶えません。

4.3 GraphQL

GraphQLは別のアプローチでこの問題を回避します。すべてのリクエストを単一のエンドポイントにPOSTし、クエリ言語で欲しいデータを記述します。

POST /graphql HTTP/1.1
Host: api.example.com
Content-Type: application/json

{ "query": "query($cat: String!) { products(category: $cat) { id name price } }", "variables": { "cat": "shoes" } }

GraphQLは強力ですが、HTTPのレベルで見ると依然としてすべてがPOSTです。HTTPキャッシュや中間インフラの恩恵を受けにくく、別のスタックと学習コストを要します。GraphQLが適さない普通のRESTサービスには過剰な選択になりえます。

4.4 回避策の比較

方式ボディ使用意味論の正確さキャッシュ標準性
POST検索エンドポイント可能低い低い慣行的
巨大GET不可普通高い非標準の直列化
GraphQL可能別モデル低い独自標準
QUERY可能高い高い標準化が進行中

5. 実務での適用 — 今何ができるか

5.1 クライアントからQUERYを送る

ほとんどのHTTPクライアントは任意のメソッド文字列を許可します。curlでは次のように送れます。

curl -X QUERY https://api.example.com/products \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{ "filter": { "category": "shoes" }, "page": { "size": 20 } }'

JavaScriptのfetch APIも任意のメソッドをサポートします。

fetch("https://api.example.com/products", {
  method: "QUERY",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ filter: { category: "shoes" } })
})

ただし一部の環境では標準にないメソッドをブロックすることがあるので、クライアント環境ごとの検証が必要です。

5.2 サーバーでQUERYを受ける

サーバーフレームワークがQUERYメソッドをルーティングとして認識するかが鍵です。標準が確定する前なので、多くのフレームワークはまだ第一級市民として対応していません。当面はカスタムメソッドルーティングを登録する方式を使います。

サーバールーティングの疑似コード

route(method="QUERY", path="/products", handler=searchProducts)

function searchProducts(request):
    criteria = parseJson(request.body)
    results = repository.search(criteria)
    response.header("Cache-Control", "max-age=60")
    return json(results)

5.3 段階的な導入戦略

いきなり全面導入するのは危険です。次のような段階的アプローチを推奨します。

段階行動
第1段階内部サービス間通信でまず試し、インフラの互換性を検証
第2段階QUERYとPOSTを同時に受けるデュアルルーティングで後方互換性を確保
第3段階クライアントSDKがQUERY非対応環境でPOSTにフォールバックするよう設計
第4段階プロキシやゲートウェイのQUERY通過可否をモニタリング

特に第2段階のデュアルルーティングが現実的な出発点です。同じハンドラにQUERYとPOSTの両方をマッピングしておけば、QUERYに対応するクライアントはより正確な意味論を得られ、そうでないクライアントは既存のPOSTで動き続けます。


6. 落とし穴と批判的な視点

6.1 中間インフラが最大の変数

QUERYの最大の現実的な障壁は**中間インフラ(intermediaries)**です。インターネットには数多くのプロキシ、ロードバランサー、CDN、ファイアウォール、APIゲートウェイがあり、これらは自分が知らないHTTPメソッドに出会うと保守的に振る舞う傾向があります。

  • 一部のプロキシは未知のメソッドをそのまま拒否(405または501)します。
  • 一部はボディを持つ安全なメソッドを想定できず、ボディを欠落させることがあります。
  • セキュリティ機器は非標準メソッドを潜在的な攻撃とみなしてブロックすることがあります。

標準がRFCとして確定し、主要なインフラベンダーがこれを実装するまで、公開インターネットを横断する経路でQUERYを安定して使うのは困難です。そのため初期導入は制御可能な内部ネットワークから始めるのが現実的です。

6.2 フレームワークとツール対応の未成熟

現時点でQUERYを第一級としてサポートするウェブフレームワークは多くありません。ルーティング、ミドルウェア、ボディパース、OpenAPIドキュメント化ツールがすべて新しいメソッドを認識するよう更新される必要があります。このエコシステムの移行には時間がかかります。

6.3 本当に新しい動詞が必要かという懐疑論

批判的な声もあります。「POSTでもうまく動くのに、わざわざ新しいメソッドが必要なのか」という懐疑論です。実際、数多くの検索APIがPOSTで何年も問題なく運用されてきました。

これに対する反論は明確です。意味論は単なる形式ではなく、インフラが正しく動作するための契約です。検索が安全で冪等であるという事実をHTTPのレベルで宣言できれば、キャッシュ、再試行、ルーティング、モニタリングが自動的に正しく動作します。POSTはこの情報を隠しますが、QUERYは明らかにします。長期的にこの違いは小さくありません。

6.4 キャッシュ仕様の未確定

先ほど見たボディベースのキャッシュは魅力的ですが、まだ完全に合意されていない領域です。キャッシュキーの正規化、Content-Location の活用、中間キャッシュの動作は、ドラフトが進化するにつれて変わる可能性があります。キャッシュを中心的な価値としてQUERYを導入するなら、仕様確定までは保守的にアプローチすべきです。

6.5 落とし穴のまとめ

落とし穴対応策
中間インフラ非互換内部ネットワーク優先導入、経路上のインフラ事前検証
フレームワーク非対応カスタムルーティング登録、デュアルルーティングで担保
キャッシュ仕様未確定キャッシュは保守的に、最新ドラフトを追跡
懐疑論と組織の抵抗意味論的利点をデータと事例で説得

7. 総合比較ダイアグラム

三つの動詞が検索リクエストを処理する流れを単純化すると次のようになります。

[クライアント] --- 検索リクエスト ---> [HTTPメソッド] ---> [サーバー]

GET   : ボディなし、すべての条件がURLに露出
        安全 / 冪等 / キャッシュ可能
        しかし長さ制限と表現力の限界

POST  : ボディあり、表現力は十分
        しかし安全でない / 冪等でない / キャッシュ不可
        インフラが書き込みと誤解する

QUERY : ボディあり、表現力は十分
        安全 / 冪等 / キャッシュ可能(条件付き)
        意味論と表現力を同時に満たす

この図がQUERYの存在理由を一枚で要約します。QUERYはGETの意味論的な正確さとPOSTの表現力を一つの動詞に収めます。


おわりに

HTTP QUERYメソッドは華やかな新技術ではありません。むしろHTTPが最初から空けておいた空白を、遅ればせながら埋める作業に近いものです。「ボディを持つ安全なメソッド」という単純なアイデアですが、その単純さがREST API設計者たちが何年も甘受してきた妥協を解消します。

もちろん今すぐすべての検索APIをQUERYに変えることはできません。標準がRFCとして確定する必要があり、プロキシとフレームワークが追いつく必要があり、キャッシュ仕様が仕上がる必要があります。しかし方向は明確です。GETの限界とPOSTのずれの間で悩んでいた長年のジレンマが、ついに意味論的に正しい動詞を手にするのです。

今できることは明確です。IETFドラフトの進行状況を追跡し、内部サービスで小さく実験し、デュアルルーティングで後方互換性を確保しておくことです。そうすれば標準が確定する瞬間に自然に移行できます。HTTPの新しい動詞が定着していく過程を、私たちは今リアルタイムで見守っています。


参考資料