断り書き:俺はN5レベルで日本語を勉強中です。間違いがあるかもしれないけど、頑張っています!
これ、もう何百回もやったことがあるはずだ:検索リクエストが大きくなりすぎた — フィルターが多すぎて、ネストしたファセットのツリーがあって、チェックボックスの値が40個もあって — もう URL に全部は入らない。だからボディに入れて POST で送った。でも POST を選んだそのとき、クライアントとサーバーの間にいる全部のキャッシュ、プロキシ、CDN、リトライ層に、同じ静かなうそをついた — このリクエストは何かを変えるかもしれないから、キャッシュするな、そして勝手にリトライもするな と。でも、それはどれも本当じゃない。検索は読み取りだけで、何度でも同じように繰り返せる。コードを書いたときから、それは分かってたはずだ。だけど POST は、ボディを運べる、みんなが使える、ただ一つのメソッドだった。だからその「あぶないかも」という考えかたも、ぜんぶそのままもらってしまったんだ。
先月、それがただ一つのやり方じゃなくなった。2026年6月、IETF が RFC 10008「The HTTP QUERY Method」 を出した — Standards Track の Proposed Standard だ。これは何年も前に、あの格好悪い名前の draft-ietf-httpbis-safe-method-w-body として始まって、ワーキンググループで14回も直されて、やっとできあがったものだ。QUERY はもう本物の、ちゃんと登録(とうろく)されたHTTPメソッドだ。そして、君のスタックはきっとまだ、これを話せない。
動画で見る: この記事の内容を YouTube で解説しています。
エンジニアがいつもごちゃまぜにする3つの言葉 #
3つの、大事なことだ。俺たちのほとんどが、この3つの言葉を同じ意味で使っているけど、本当は同じじゃないからだ。
安全(safe) っていうのは、クライアントから見て、そのリクエストが読み取りだけ、という意味だ — サーバーの中の何かを変えてくれと頼んでるんじゃなくて、何かを教えてくれと頼んでる。GET は安全だ。冪等(べきとう / idempotent) っていうのは、同じリクエストをN回送っても、1回送ったのと同じところに着く、という意味だ — 10回リトライした結果が、1回の結果と同じになる。これはまさに、つながりが切れたあとにリトライ層が、何も気にしないでリクエストをもう一度送れる、あのはたらきだ。GET は冪等だし、変な話 DELETE も冪等だ(同じものを2回消しても、消えているのは同じだ)。キャッシュ可能(cacheable) っていうのは、レスポンスを保存しておいて、あとから来たリクエストに、オリジンまで戻らずにそのまま返せる、という意味だ。
この3つが頭の中でいつもセットになってるのは、GET が3つ全部を持ってるからだ。だから今まで、分ける必要がなかった — でも、ボディが必要になると、この3つはバラバラになる。
空いていたマス #
2×2の表を書いてみよう。よこの線は「リクエストのボディを運ぶか」。たての線は「安全で冪等か」。
quadrantChart
title HTTP メソッド — ボディ と「安全・冪等」
x-axis ボディなし --> ボディを運ぶ
y-axis 危険 / 書き込み --> 安全・冪等
quadrant-1 QUERY がここに入る
quadrant-2 読み取り、使えるボディなし
quadrant-3 危険、ボディなし
quadrant-4 書き込み、ボディあり
GET: [0.24, 0.82]
POST: [0.80, 0.18]
PUT: [0.70, 0.30]
DELETE: [0.63, 0.36]
QUERY: [0.82, 0.86]
GET は「安全で冪等」のかどにいるけど、使えるボディを持ってない — その理由はすぐあとで話す。POST はボディを運べる、中身は何でもいい、でも「あぶないかもしれない」と決められていて、冪等だとも約束されていないから、ちょうど反対のかどにいる。PUT と DELETE は冪等だけど、あれは読み取りじゃなくて書き込みだ。そして、本当に欲しかったマス — 安全で、冪等で、しかもボディを運べる — は空っぽだった。20年ものあいだ、「これは読み取りで、繰り返せて、それにパラメータが URL に入らない」という意味の標準(ひょうじゅん)メソッドは、なかった。道具が2つあって、どっちも合わなくて、だから見た目が近いほうの、まちがった道具をつかんだんだ。
QUERY はまさに、その空っぽのマスを埋める。HTTP Method Registry に、Safe = yes、Idempotent = yes で登録されていて — そしてボディを取る。
なぜ「ボディ付きのGET」は最初から本物じゃなかったのか #
数ヶ月に一度、誰かがスレッドで当たり前のハックを出してくる:GET は読み取りメソッドなんだから、ボディを GET に付ければいいじゃん、と。10秒くらいは、かしこく聞こえる。
でも、そうじゃない理由はこうだ。RFC 9110 — HTTP のいちばん大事な決まり — は、クライアントは GET リクエストで content を作る SHOULD NOT(作るべきじゃない)と言っていて、GET の content にははっきり決められた意味(セマンティクス)がない、と書いてある。これは「おすすめしないけど動く」じゃない。「その決まりは、君のボディに何の意味もあげない」ということだ。つまり、通り道にいる全部の実装(じっそう)が、そのボディを好きなように使っていい。しかも決まりは、もっと先まで言っている:ボディ付きの GET は一部の実装に、リクエストをことわらせて、つながりを切らせるかもしれないとちゅういしている。持つはずのないメソッドにボディが付いているのは、よくあるリクエストスマグリングのやり方だからだ。だから、決まりのレベルでの答えはこうだ — ボディは意味がないかもしれないし、つながりは地面にたたき落とされるかもしれない。(別の話として — これは RFC の文じゃなくて、本当に起きることだけど — 多くのプロキシや中間の機械は、GET のボディを、オリジンに届く前に、そっと取ってしまう。これはこれで、とくべつな種類の、デバッグでつらい午後になる。)「ボディ付きの GET」は、最初から本物のえらべる道じゃなかった — 読み取りメソッドの服を着た、未定義動作(みていぎどうさ)だったんだ。
検索にPOSTを使うのは、軽いがまんじゃなくてレベルダウンだ #
だからみんな、かわりに POST に手をのばした — でも POST を選ぶのは、感じるほど「どっちでもない」選び方じゃない。
Elasticsearch の _search を見てみよう。これを読んでる全員が、こういう形のものを一度は出したことがあるからだ。Query DSL はネストした JSON の形で — bool クエリ、ネストした集約(しゅうやく)、フィルター句 — URL のパラメータでは書けない。だからドキュメントは、それをリクエストのボディに入れるようにすすめる。すごく当たり前のことだ。でも、そのボディを運ぶために使う動詞は POST だ。そして、そもそもここに来た理由の一つが、じっさいの URL の長さのかぎりにある:ブラウザやプロキシは、じっさいの URL が2キロバイトあたりで、よくうまく動かなくなる(一つの決まった数字はないし、正しくてどこでも通じるバイトの上のかぎりを教えてくるやつは、うそを言っている)。だから大きなフィルターの集まりは、どのみち URL の文字列の中では生きのこれなかった。
だから POST する。そして、これがそのツケだ。POST は「あぶないかもしれない」と決められているから、通り道の全部のキャッシュは、レスポンスを、はじめから保存できないものとしてあつかう — さっき計算した高い集約が、30秒あとの同じクエリにさえ、もう一度使えない。POST は「冪等だよ」とは言っていないから、ちゃんとしたリトライ層は、ネットワークがちょっと切れたあとに、それを自動でもう一度送らない。もう一度送ったら、だれかに二重にお金を取るかもしれない、とリトライ層は思うからだ — その検索は、何回送っても、まったく同じなのに。こんなの、たのんでない。ぜんぶ、ボディを運ぶために、まちがった動詞を選んだせいで、いっしょについてきたものだ。本当に、しかもそれを見せることもできる、ただの読み取りのリクエストなのに。
QUERY を、きちんと #
QUERY は、本当のことを言うメソッドだ。RFC のアブストラクトから、そのまま引く:“A QUERY requests that the request target process the enclosed content in a safe and idempotent manner and then respond with the result of that processing. This is similar to POST requests, but QUERY requests can be automatically repeated or restarted without concern for partial state changes."(QUERY は、リクエストの相手が、中に入れた content を安全で冪等なやり方で処理(しょり)して、その結果を返すことをお願いする。これは POST リクエストに似ているけど、QUERY リクエストは、一部だけ変わることを気にしないで、自動でくり返したり、やり直したりできる。)ボディを運ぶところは POST に似ている。でも、POST にはぜったいにできなかった2つの約束を、決まりが QUERY にあげているところが、POST とちがう。
本当に大事なのは、2つのポイントだ。
1つ目は、キャッシュキーだ。QUERY のレスポンスはキャッシュ可能で、RFC 10008 の §2.7 は、かならず守るルール(MUST)として、こう決めている — QUERY リクエストのキャッシュキーには、リクエストの content(ボディ)と、それに関係する情報を、必ず入れなければならない、URI だけじゃなくて。キャッシュは昔からずっと、URL をキーにしてきた。URL がリクエストをぜんぶ見分けられるなら、それでいい — でも、ボディを運ぶ読み取りは、同じ URL が2つのちがう質問を指していて、見分けられるのはボディだけだ。ボディをキャッシュキーの中に入れることで、QUERY は、ボディを運ぶリクエストのキャッシュを、はじめてちゃんと決まったものにする。同じ URL に、ちがうボディで送られた2つの QUERY リクエストは、正しく2つのちがうキャッシュエントリーになる。
2つ目は Content-Location で、これはしずかにきれいなほうだ。うまくいった QUERY のレスポンスは、URI を指す Content-Location ヘッダーを入れても良い(MAY) — そして §2.3 によると、これはサーバーがこう言っている、ということだ:この URI に、ただの GET を送れば、今やったクエリの結果を取り出せる、と。つまりサーバーは、こっちのごちゃごちゃした40個のフィルターの検索を受け取って、同じ結果になる、ふつうの、GET できる、どこでもキャッシュできる URL を返せる。むずかしいクエリが入って、きれいでキャッシュできるリソースが出てくる。(知っておくといいガードレールが1つ:Content-Type フィールドが無いか、ボディと合っていない場合、サーバーはリクエストを必ず失敗させなければならない(MUST) — QUERY は、何を送っているのかをあいまいにすることをゆるさない。)
これが君のAPIでどう見えるか #
検索エンドポイントが、あやまるのをやめる。今はこう書く:
POST /products/search HTTP/1.1
Content-Type: application/json
{ "filters": { "category": "climbing", "price": { "lte": 200 } },
"sort": "rating:desc" }
— そして書きながら分かってる、レスポンスはキャッシュされないし、安全にリトライもされない。POST だから。
QUERY 版はワイヤーの上ではほとんど同じで、約束する中身はまったくちがう:
QUERY /products/search HTTP/1.1
Content-Type: application/json
{ "filters": { "category": "climbing", "price": { "lte": 200 } },
"sort": "rating:desc" }
HTTP/1.1 200 OK
Content-Type: application/json
Content-Location: /products/search/results/9f2c1a
Cache-Control: max-age=60
{ "results": [ ... ] }
同じボディ、同じ気持ち — でも今度は、レスポンスは正直にキャッシュできて(あのボディをキーにして)、リトライ層はつながりが切れたあと自由にもう一度送れて、サーバーはまったく同じ結果のために、正しい、GET できる URL を出した。リクエストはやっと、自分が何なのかを言えて、だれもキャッシュにうそをつかなくてよくなった。
落とし穴:みとめられたことと、デプロイされたことはちがう #
さて、正直なところだ。ここまでの全部は決まりの話で、決まりは君の本番(ほんばん)の通り道じゃないからだ。
QUERY のねうちは、君のスタックのどれだけがそれを分かっているかで決まる。そして2026年のなかごろ、ほとんどは分かっていない。クライアント、サーバーのフレームワーク、リバースプロキシ、WAF、CDN — それぞれが、QUERY リクエストが行って帰ってくるのを丸ごと生きのびる前に、本物のサポートをそだてないといけない。そして今日、その多くは、知らないメソッドを中身の分からないトークンとしてあつかって、そのまま通すか、もっと悪いと、リストに無いものとしてはっきりことわる。この最後のやつが、君をたたき起こす:QUERY を聞いたことのない WAF が、それをおかしなトラフィックとしてブロックする。今日どのツールのどのバージョンが正しく動くかを、名前を出して言うのは言うべきじゃない — サポートのようすは週ごとに動いているから — だからやらない。言うねうちのある安全な、だいたいの話は、1つだけだ:知らないメソッドは、中間の機械をただのトークンとしてすりぬけるかもしれない。でも、キャッシュや冪等のセマンティクスは、いっしょにはすりぬけない — それは、通り道のどれかの箱が、じっさいに QUERY を実装したときにだけ出てくる。
ブラウザはブラウザで、別の話がある。Fetch 標準の CORS ルールでは、QUERY はセーフリストにのっているメソッドじゃないから、クロスオリジンの QUERY は preflight を起こす — でもこれは本当に、新しい税金じゃない。クロスオリジンの application/json の POST は、その Content-Type のせいで、もう preflight する。本当に気をつけるところは、QUERY を Fetch にちゃんと組み込むのは、まだやっているとちゅうだ、ということだ — そのための WHATWG Fetch の issue(whatwg/fetch #1938)は、これを書いている今もまだ開いている — だからこれは「今日 fetch(url, { method: 'QUERY' }) を呼んで出す」じゃない。まだだ。
そしてやっぱり、新しいメソッドは新しくねらわれるところだ。ボディをキーにするキャッシュは、URL をキーにする世界が考えなくてよかったキャッシュポイズニングの問題を開くし、知らないメソッドはリクエストスマグリングの新しいきっかけになるし、QUERY をよまない全部の WAF は、対応するまでは見えないところになる。どれも、さける理由にはならない — 目を開けて出していく理由になるだけだ。スイッチを入れて祈るんじゃなくて。ついでに言っておくと、RFC を書いた人の何人かは、CDN やエッジのインフラで働いている。だから QUERY のエッジそうでの対応が、君の web フレームワークより先に来ても、俺はおどろかない — でもこれは、サポートがどこに先に出てくるかについての、俺がそう思うだけの話だ。だれかが約束したロードマップじゃない。
だから答えは、2つに分かれる。今から QUERY に向けて作っていこう — 検索エンドポイントを、安全で、冪等で、ボディを運ぶ読み取りとして考えればいい。だってそれは最初からずっとそういうものだったし、今はそう言うためのことばができたんだから。でも、はしからはしまで QUERY にたよるのは、自分のじっさいの通り道 — クライアント、プロキシ、WAF、CDN、オリジン — を QUERY リクエストが本当に通りぬけて、正しく返ってくるのを見てからにしよう。
大事なのは、新しいピカピカの動詞で遊べることじゃない。QUERY は、ボディを運ぶ読み取りが、何十年もずっと俺たちみんなの下にあったキャッシュとリトライのしくみに、本当のことを言えるようにする、はじめての HTTP メソッドだ。クエリが URL からあふれて、ボディをワイヤーに通すために冪等についてうそをつくのはもういやだ、というときに、これに手をのばせばいい。