Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/plug/csrf_protection.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion lib/plug/parsers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion lib/plug/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions test/plug/csrf_protection_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
10 changes: 10 additions & 0 deletions test/plug/parsers_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand Down
9 changes: 9 additions & 0 deletions test/plug/router_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"))
Expand Down