【Rails】HTTPのQUERYメソッドとは?GET・POSTとの違いから実装まで解説

塩見直樹
塩見直樹約11分で読めます
はてなブックマーク

はじめに

こんにちは、株式会社TOKOSのナオキです!

今回扱うのは、2026年6月に正式仕様となった新しいHTTPのQUERYメソッドです!

検索APIを作るとき「GETにすべきか、POSTにすべきか」で悩んだことはありませんか?
GETだとURLの長さ制限等の規約があり、POSTだと「検索なのに何かの処理をしていそう」に見えてしまいます。

QUERYは、その両方の弱みを補うために作られたメソッドです。
ぶっちゃけ、実プロダクトにて利用するには時期尚早かもしれないです🥲
ただ、先んじてキャッチアップしておいた方が良さそうな情報なので今回記事にしました!

対象読者

  • 検索APIをGETにするかPOSTにするか悩んでいる方
  • RailsでAPIを実装している方
  • QUERYについて、何が嬉しいのか分からない方

この記事で扱う内容

  • GETとPOSTでは何が足りなかったのか
  • QUERYメソッドについて
  • 実際にcurlとRailsで動かしてみた結果(Pumaで盛大にハマりました)
  • 仕様の細かいところ
  • 今の時点で実務に使えるのか

GETとPOSTの問題点

GETの問題点

QUERYの仕様は、インターネット技術の標準化を促進する団体のIETFが、RFC 10008として公開しています。
この中で、GETで検索を書こうとしたときの問題を4つ挙げています。

https://www.rfc-editor.org/info/rfc10008/www.rfc-editor.org

1. URLの長さ制限が事前に分からない

ブラウザからサーバまで、途中で何台ものコンピュータを通ることがあるので、どこかで切られるかもしれません。HTTPは「最低8000バイトは通してほしい」と勧めていますが、それ以上は保証されません。

2. パーセントエンコードによってURLの文字数が多くなる

URLに使えない日本語や記号は%E8のような形に変換されます。この変換によってURLが無駄に膨らんでしまいます。

3. URLはログやブックマークに残りやすい

検索条件にメールアドレスや氏名が入っていたら、それがそのままアクセスログに残ってしまいます。

ナオキ
ナオキ

逆に、検索結果をブックマークできるのはGETの良いところでもあります!
条件を入力し直さなくても、URLを開くだけで同じ条件で再検索できます。

4. 条件の組み合わせで別URLになってしまう

条件をURLに埋めると、ありうる条件の組み合わせすべてが別々のリソース(別々のURL)として扱われます。

POSTの問題点

そこで多くのAPIがPOST /products/searchのような形を選んできました。ボディが使えるので、上の4つは全部解決します!

ただ、HTTPから見ると別の問題が出ます。POSTは仕様上「サーバの状態を変えるかもしれない」メソッドなので、中身がただの検索であることを外から判断できません。

GETにボディを付ける方法の問題点

ここまで読んで「GETにボディを付ければ良いのでは?」と思った方もいるかもしれません。実際付けること自体は可能です。

ただしGETのボディに決まった意味はありません。途中のプロキシやCDNが無視や拒否をしても、仕様違反にはなりません。RFC 9110も、原則として付けないよう求めています。

「安全」と「冪等」

ここでQUERYの説明に必要な用語を2つだけ整理します!

1. 安全(safe)

「サーバの状態を変えない」という意味です。
GET /users/1はユーザー情報を読むだけなので安全、DELETE /users/1は消すので安全ではありません。
安全なメソッドなら、誰が勝手に叩いてもデータは変わりません。逆に削除処理をGETで作ってしまうと、リンクを開いただけでデータが消える事故につながります。

2. 冪等(idempotent)

「同じリクエストを何回投げても結果が同じ」という意味です。
PUT /users/1で名前を「ナオキ」にする操作は、何度実行しても最終状態は同じなので冪等です。
POST /usersでユーザーを作る操作は、10回投げれば10人できるので冪等ではありません。

QUERYメソッドとは

QUERYは、GETと同じく安全かつ冪等でありながら、POSTのようにボディへ検索条件を書けるメソッドです!

安全で冪等なメソッド

仕様には次のように書かれています。

QUERY requests are safe with regard to the target resource ... Furthermore, QUERY requests are idempotent; they can be retried or repeated when needed, for instance, after a connection failure.
(QUERYリクエストは対象リソースに対して安全である。さらにQUERYリクエストは冪等であり、接続が切れたときなど、必要に応じて再送・再実行できる)

つまり「これは読むだけの操作なので、失敗したら勝手に投げ直していいですよ」と、HTTPのレベルで宣言できるということです。POSTは同じデータが二重に登録されるおそれがあるので投げ直せません。

GET・POSTとの比較

以下、RFC 10008の比較表(日本語訳)となります。

GETQUERYPOST
安全はいはいそうでない可能性がある
冪等はいはいそうでない可能性がある
クエリ自体のURIあり(定義上)任意(Location)なし
クエリ結果のURI任意(Content-Location)任意(Content-Location)任意(Content-Location)
キャッシュ可能はいはいはい(ただし後続のGET/HEAD向けのみ)
ボディの意味定義されていない想定されている想定されている

POSTが「そうでない可能性がある」と書かれているのは、POSTが常に危険なのではなく「安全かどうかを外から判断できない」ということです。

curlでQUERYを送る

curlは-Xで任意のメソッドを指定できるので、そのまま投げられます。-Hはリクエストヘッダ、-dはボディの指定です。

ターミナル
curl -X QUERY http://localhost:3000/products \
  -H "Content-Type: application/json" \
  -d '{"category":"shoes","color":"red","min_price":3000}'

見た目はPOSTとほぼ同じですが、先程の比較表の通り意味がまったく違います!
ちなみにQUERYではボディにContent-Typeを付けるのが必須です。付け忘れたリクエストは、サーバ側が必ずエラーにしなければならないと仕様で決められています。

RailsにQUERYを投げてみる

ここからはRailsの話になるので、触っていない方は次の章まで読み飛ばしてもらって大丈夫です。ただ、ハマった箇所があるので、他の言語を使っている方にも「そういう罠がある」という認識を持ってもらえると嬉しいです!

まずPumaに弾かれる

Railsアプリを起動した状態で、先ほどのcurlを投げてみました。
すると、こんなレスポンスが返ってきました。

レスポンス
HTTP/1.1 501 Not Implemented
connection: close
 
Puma caught this error: QUERY method is not supported (Puma::HttpParserError501)

Pumaは標準的なメソッドしか受け付けず、それ以外はアプリへ届く前に501で切断します。つまりRailsのコードをどう書いても関係なく弾かれます。

ナオキ
ナオキ

ここに気づかず、ルーティングを疑い続けていました🥲

解決するには、Pumaの設定で許可するメソッドを増やします!

config/puma.rb
supported_http_methods %w[HEAD GET POST PUT PATCH DELETE OPTIONS QUERY]

次にRailsで弾かれる

これで通ると思ったら、今度は別のエラーが返ってきました。

エラーメッセージ
ActionController::UnknownHttpMethod: QUERY, accepted HTTP methods are
OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT, PROPFIND, PROPPATCH,
MKCOL, COPY, MOVE, LOCK, UNLOCK, VERSION-CONTROL, REPORT, CHECKOUT, CHECKIN,
UNCHECKOUT, MKWORKSPACE, UPDATE, LABEL, MERGE, BASELINE-CONTROL, MKACTIVITY,
ORDERPATCH, ACL, SEARCH, MKCALENDAR, and PATCH

Pumaは501、RailsはUnknownHttpMethod(多くの環境では405)と、弾かれ方が違うので手間取りました。
許可されているメソッドの一覧を見ると、QUERYが入っていません。

Rails 8.1系で試すなら、自分でメソッドを登録します。ファイル名は何でも良く、config/initializers/配下にあれば起動時に読み込まれます。

config/initializers/query_method.rb
ActionDispatch::Request::HTTP_METHOD_LOOKUP["QUERY"] = :query

そしてqueryというルーティングヘルパーはまだ無いので、matchでvia: :queryを指定して受けます。
HTTP_METHOD_LOOKUPに"QUERY"を登録済みなら、QUERYを通すことができます。ルート定義はconfig/routes.rbに書きます。

config/routes.rb
Rails.application.routes.draw do
  match "/products", to: "products#search", via: :query
end

コントローラはActionController::API配下にしました。

app/controllers/products_controller.rb
class ProductsController < ActionController::API
  def search
    render json: {
      total: 2,
      items: [
        { id: 1, name: "商品A", price: 1000 },
        { id: 2, name: "商品B", price: 2000 }
      ]
    }
  end
end

ここまでやって、ようやく通りました✨

レスポンス
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
 
{"total":2,"items":[{"id":1,"name":"商品A","price":1000},{"id":2,"name":"商品B","price":2000}]}

もう少し詳しく知りたい人向け

ここからは仕様の細かい部分です。実際にQUERYを実装するときの参考にしてください!

Content-LocationヘッダとLocationヘッダの違い

この2つは名前が似ていて混乱しますが、指しているものが違います!

  • Content-Locationは、今返した検索結果そのものの置き場所です。このURLをGETすれば同じ結果が得られます
  • Locationは、その検索操作と等価なリソースです。このURLをGETすれば検索が再実行されます

例えば「3,000円以上」をQUERYで検索したとします。Content-LocationのURLを後からGETすると、検索した時点の結果がそのまま返ります。
LocationのURLをGETすると、同じ条件で検索し直すので、その間に追加された商品も結果に含まれます。

Accept-Queryヘッダ

どんな形式を受け付けるかを伝えるレスポンスヘッダです。

Accept-Queryヘッダ
Accept-Query: "application/jsonpath", application/sql;charset="UTF-8"

上記の場合だと、application/jsonpathとapplication/sqlの2つの形式を受け付けることを伝えています。

ステータスコードの使い分け

ステータス使いどころ
415 Unsupported Media Typeそのメディアタイプに対応していない
422 Unprocessable Content形式は理解できたが、中身が処理できない
406 Not Acceptableクライアントが要求したレスポンス形式を返せない
501 Not ImplementedそもそもQUERYに対応していない

仕様上はキャッシュできるが、ブラウザ実装はこれから

QUERYのレスポンスはキャッシュできます(RFC 10008 §2.7)。ただしGETと決定的に違うのは、キャッシュのキーにリクエストの中身を含めなければならない点です。

GETはURLだけ見れば判別できますが、QUERYは同じURLでもボディが違えば別の結果になります。つまりボディを最後まで読まないとキャッシュの一致判定ができません。

ブラウザではfetch(url, { method: "QUERY", body })でQUERY自体は送れます。一方、同じQUERYを繰り返してもHTTPキャッシュに乗るかは別問題です。whatwg/fetch#1938では、現時点のChromeとFirefoxで同一QUERYがキャッシュされない、と報告されています。仕様上キャッシュできることと、実際にキャッシュされることは別なので、キャッシュ前提の設計はまだ避けた方が良いです。

今使えるのか

実務でQUERYを使うのはまだ早いです!
社内APIやサービス間通信でも、途中のプロキシやCDNがQUERYを通さないことがあります。

対応状況をざっくり分けると、こうなります。

  • アプリケーション側は、かなり進んでいる。Rails、Laravel、Spring、ASP.NET Core、Express、Fastifyなど主要なフレームワークはほぼ対応済みです。
  • インフラ側はこれから。ブラウザはfetchでQUERYを送ること自体はできますが、キャッシュやフォーム連携などの本格対応は議論中です。nginxは対応中、Amazon CloudFrontは許可メソッドが限定されておりQUERYは通せません。

細かい対応状況はエコシステム対応状況のトラッカーにまとまっています。ここは数ヶ月で変わるので、実際に使う前に必ず最新を見てください!

まとめ

  • QUERYは、GETのように安全・冪等でありながら、ボディに検索条件を書けるメソッド
  • GETにボディを付ける方法は、意味が未定義で途中の無視や拒否もありうる
  • Railsで試すなら、Pumaのsupported_http_methodsの拡張が必要
  • queryルーティングヘルパーは本体にマージ済みだが未リリース。Rails 8.1系以前ではHTTP_METHOD_LOOKUPへの登録とvia: :queryが必要
  • アプリケーション側の対応は進んでいるが、CloudFrontなど許可メソッドが限定される層はこれから

この記事を書いた人

塩見直樹
塩見直樹

TOKOSのバックエンドエンジニア。パフォーマンス最適化と可読性の向上を目指しています。アニメ好きで、特にHUNTER×HUNTERが好きです。