From 76f0e3c9569780c470012324a35e543dbdfafea2 Mon Sep 17 00:00:00 2001 From: Pavel Solovev Date: Thu, 25 Jun 2026 10:49:57 +0300 Subject: [PATCH] Enhance Plug to support HTTP QUERY method: add `query/3` macro for routing, enable body parsing, and treat QUERY as unprotected in CSRF protection. --- CHANGELOG.md | 8 ++++++++ lib/plug/csrf_protection.ex | 2 +- lib/plug/parsers.ex | 3 ++- lib/plug/router.ex | 10 +++++++++- test/plug/csrf_protection_test.exs | 4 ++++ test/plug/parsers_test.exs | 10 ++++++++++ test/plug/router_test.exs | 9 +++++++++ 7 files changed, 43 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcfda630..0d247066 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## v1.21.0 + +### Enhancements + + * [Plug.Router] Add the `query/3` macro for routing HTTP QUERY (RFC 10008) requests + * [Plug.Parsers] Parse request bodies for the HTTP QUERY method (RFC 10008) + * [Plug.CSRFProtection] Treat the safe, idempotent HTTP QUERY method (RFC 10008) as unprotected + ## v1.20.1 (2026-06-23) ### Bug fixes diff --git a/lib/plug/csrf_protection.ex b/lib/plug/csrf_protection.ex index b5e6180e..cc5467ff 100644 --- a/lib/plug/csrf_protection.ex +++ b/lib/plug/csrf_protection.ex @@ -108,7 +108,7 @@ defmodule Plug.CSRFProtection do alias Plug.Crypto.MessageVerifier @behaviour Plug - @unprotected_methods ~w(HEAD GET OPTIONS) + @unprotected_methods ~w(QUERY HEAD GET OPTIONS) @digest Base.url_encode64("HS256", padding: false) <> "." # The token size value should not generate padding diff --git a/lib/plug/parsers.ex b/lib/plug/parsers.ex index baf48441..6246526e 100644 --- a/lib/plug/parsers.ex +++ b/lib/plug/parsers.ex @@ -70,6 +70,7 @@ defmodule Plug.Parsers do This plug only parses the body if the request method is one of the following: + * `QUERY` * `POST` * `PUT` * `PATCH` @@ -247,7 +248,7 @@ defmodule Plug.Parsers do | {:next, Conn.t()} @behaviour Plug - @methods ~w(POST PUT PATCH DELETE) + @methods ~w(QUERY POST PUT PATCH DELETE) @impl true def init(opts) do diff --git a/lib/plug/router.ex b/lib/plug/router.ex index e8bcaa1e..d49a8c40 100644 --- a/lib/plug/router.ex +++ b/lib/plug/router.ex @@ -44,7 +44,7 @@ defmodule Plug.Router do In the example above, a request will only match if it is a `GET` request and the route is "/hello". The supported HTTP methods are - `get`, `post`, `put`, `patch`, `delete` and `options`. + `get`, `query`, `post`, `put`, `patch`, `delete` and `options`. A route can also specify parameters which will then be available in the function body: @@ -389,6 +389,14 @@ defmodule Plug.Router do compile(:head, path, options, contents, __CALLER__) end + @doc """ + Dispatches to the path only if the request is a QUERY request. + See `match/3` for more examples. + """ + defmacro query(path, options, contents \\ []) do + compile(:query, path, options, contents, __CALLER__) + end + @doc """ Dispatches to the path only if the request is a POST request. See `match/3` for more examples. diff --git a/test/plug/csrf_protection_test.exs b/test/plug/csrf_protection_test.exs index e751f0e6..ced85a48 100644 --- a/test/plug/csrf_protection_test.exs +++ b/test/plug/csrf_protection_test.exs @@ -205,6 +205,10 @@ defmodule Plug.CSRFProtectionTest do refute conn.halted refute get_session(conn, "_csrf_token") + conn = call(conn(:query, "/")) + refute conn.halted + refute get_session(conn, "_csrf_token") + conn = call(conn(:head, "/")) refute conn.halted refute get_session(conn, "_csrf_token") diff --git a/test/plug/parsers_test.exs b/test/plug/parsers_test.exs index 2211f01b..ba05db42 100644 --- a/test/plug/parsers_test.exs +++ b/test/plug/parsers_test.exs @@ -150,6 +150,16 @@ defmodule Plug.ParsersTest do assert conn.params["foo"] == "baz" end + test "parses QUERY request bodies" do + conn = + conn(:query, "/?foo=bar", "foo=baz") + |> put_req_header("content-type", "application/x-www-form-urlencoded") + |> parse() + + assert conn.params["foo"] == "baz" + assert conn.body_params["foo"] == "baz" + end + test "parses multipart bodies with test params" do conn = parse(conn(:post, "/?foo=bar")) assert conn.params == %{"foo" => "bar"} diff --git a/test/plug/router_test.exs b/test/plug/router_test.exs index 705f2f9c..5651f70c 100644 --- a/test/plug/router_test.exs +++ b/test/plug/router_test.exs @@ -175,6 +175,10 @@ defmodule Plug.RouterTest do resp(conn, 200, "") end + query "/query" do + resp(conn, 200, "") + end + post "/post" do resp(conn, 200, "") end @@ -562,6 +566,11 @@ defmodule Plug.RouterTest do assert conn.status == 200 end + test "declare and call QUERY requests" do + conn = call(Sample, conn(:query, "/query")) + assert conn.status == 200 + end + test "forwards only :via methods" do for method <- [:post, :put] do resp = call(Forward, conn(method, "/forward_via"))