From a18a0d449e4d869b75b87a1075fd8c79e86d8d28 Mon Sep 17 00:00:00 2001 From: Mandar Joshi Date: Tue, 3 Nov 2026 13:25:34 +0530 Subject: [PATCH 1/4] Match whole segments for statics in verified routes Closes #6895. --- CHANGELOG.md | 4 +++ lib/phoenix/verified_routes.ex | 4 ++- test/phoenix/verified_routes_test.exs | 37 +++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87e0d54435..e04722ab15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ ([#5509](https://github.com/phoenixframework/phoenix/issues/5509)) * [Phoenix.Socket] Type `topic` and `channel_pid` as possibly `nil` in `Phoenix.Socket.t`, as they are before a channel is joined + * [Phoenix.VerifiedRoutes] Only treat paths as static when their first + segment is one of the `:statics`, so routes such as `~p"/images-gallery"` + are verified and get the path prefixes + ([#6895](https://github.com/phoenixframework/phoenix/issues/6895)) ## Enhancements diff --git a/lib/phoenix/verified_routes.ex b/lib/phoenix/verified_routes.ex index d43f5f3338..4c3b6f0f07 100644 --- a/lib/phoenix/verified_routes.ex +++ b/lib/phoenix/verified_routes.ex @@ -1076,8 +1076,10 @@ defmodule Phoenix.VerifiedRoutes do Module.get_attribute(env.module, name) || raise "expected @#{name} module attribute to be set" end + # Like Plug.Static's :only, the whole first segment must match defp static_path?(path, statics) do - Enum.find(statics, &String.starts_with?(path, "/" <> &1)) + [_, segment | _] = String.split(path, ["/", "#"], parts: 3) + segment in statics end defp build_own_forward_path(conn, router, path) do diff --git a/test/phoenix/verified_routes_test.exs b/test/phoenix/verified_routes_test.exs index 8eaf56e42f..0fa907afd9 100644 --- a/test/phoenix/verified_routes_test.exs +++ b/test/phoenix/verified_routes_test.exs @@ -255,6 +255,23 @@ defmodule Phoenix.VerifiedRoutesTest do assert PathPrefixes.images_baz() == "/images/baz" end + test ":statics match whole path segments" do + defmodule StaticSegments do + use Phoenix.VerifiedRoutes, + endpoint: unquote(@endpoint), + router: unquote(@router), + path_prefixes: [{__MODULE__, :locale, []}], + statics: ["fo"] + + def locale, do: "en" + def foo, do: ~p"/foo" + def foo_url, do: url(~p"/foo") + end + + assert StaticSegments.foo() == "/en/foo" + assert StaticSegments.foo_url() == "https://example.com/en/foo" + end + test "unverified_path" do assert unverified_path(conn_with_script_name(), @router, "/posts") == "/api/posts" assert unverified_path(@endpoint, @router, "/posts") == "/posts" @@ -734,6 +751,26 @@ defmodule Phoenix.VerifiedRoutesTest do :code.delete(__MODULE__.Unmatched) end + test "~p warns on unmatched path starting with a static name" do + warnings = + ExUnit.CaptureIO.capture_io(:stderr, fn -> + defmodule UnmatchedStatic do + use Phoenix.VerifiedRoutes, + endpoint: unquote(@endpoint), + router: unquote(@router), + statics: ["images"] + + def test, do: ~p"/images-unknown" + end + end) + + assert warnings =~ + ~s|no route path for Phoenix.VerifiedRoutesTest.Router matches "/images-unknown"| + after + :code.purge(__MODULE__.UnmatchedStatic) + :code.delete(__MODULE__.UnmatchedStatic) + end + test "~p warning location resolves to the current directory" do {_, diagnostics} = Code.with_diagnostics(fn -> From 95ebc59915c144d280cd6a04adad1721f1c3d566 Mon Sep 17 00:00:00 2001 From: Mandar Joshi Date: Tue, 3 Nov 2026 21:52:24 +0530 Subject: [PATCH 2/4] Keep multi-segment statics working --- lib/phoenix/verified_routes.ex | 10 ++++++---- test/phoenix/verified_routes_test.exs | 4 +++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/phoenix/verified_routes.ex b/lib/phoenix/verified_routes.ex index 4c3b6f0f07..a69ba444a2 100644 --- a/lib/phoenix/verified_routes.ex +++ b/lib/phoenix/verified_routes.ex @@ -1076,10 +1076,12 @@ defmodule Phoenix.VerifiedRoutes do Module.get_attribute(env.module, name) || raise "expected @#{name} module attribute to be set" end - # Like Plug.Static's :only, the whole first segment must match - defp static_path?(path, statics) do - [_, segment | _] = String.split(path, ["/", "#"], parts: 3) - segment in statics + # Statics must match whole segments, so "images" matches + # "/images/logo.png" but not "/images-gallery" + defp static_path?("/" <> path, statics) do + Enum.any?(statics, fn static -> + path == static or String.starts_with?(path, [static <> "/", static <> "#"]) + end) end defp build_own_forward_path(conn, router, path) do diff --git a/test/phoenix/verified_routes_test.exs b/test/phoenix/verified_routes_test.exs index 0fa907afd9..2fd78e1d1c 100644 --- a/test/phoenix/verified_routes_test.exs +++ b/test/phoenix/verified_routes_test.exs @@ -261,15 +261,17 @@ defmodule Phoenix.VerifiedRoutesTest do endpoint: unquote(@endpoint), router: unquote(@router), path_prefixes: [{__MODULE__, :locale, []}], - statics: ["fo"] + statics: ["fo", "images/icons"] def locale, do: "en" def foo, do: ~p"/foo" def foo_url, do: url(~p"/foo") + def icon_url, do: url(~p"/images/icons/logo.png") end assert StaticSegments.foo() == "/en/foo" assert StaticSegments.foo_url() == "https://example.com/en/foo" + assert StaticSegments.icon_url() == "https://static.example.com/images/icons/logo.png" end test "unverified_path" do From cf0675ba09db9559a1816579d427538b831ebb03 Mon Sep 17 00:00:00 2001 From: Mandar Joshi Date: Wed, 4 Nov 2026 20:56:15 +0530 Subject: [PATCH 3/4] Trim trailing slashes from statics --- lib/phoenix/verified_routes.ex | 2 +- test/phoenix/verified_routes_test.exs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/phoenix/verified_routes.ex b/lib/phoenix/verified_routes.ex index a69ba444a2..0012b2d700 100644 --- a/lib/phoenix/verified_routes.ex +++ b/lib/phoenix/verified_routes.ex @@ -257,7 +257,7 @@ defmodule Phoenix.VerifiedRoutes do statics = case Keyword.get(opts, :statics, []) do - list when is_list(list) -> list + list when is_list(list) -> Enum.map(list, &String.trim_trailing(&1, "/")) other -> raise ArgumentError, "expected statics to be a list, got: #{inspect(other)}" end diff --git a/test/phoenix/verified_routes_test.exs b/test/phoenix/verified_routes_test.exs index 2fd78e1d1c..364ce57f84 100644 --- a/test/phoenix/verified_routes_test.exs +++ b/test/phoenix/verified_routes_test.exs @@ -261,17 +261,19 @@ defmodule Phoenix.VerifiedRoutesTest do endpoint: unquote(@endpoint), router: unquote(@router), path_prefixes: [{__MODULE__, :locale, []}], - statics: ["fo", "images/icons"] + statics: ["fo", "images/icons", "fonts/"] def locale, do: "en" def foo, do: ~p"/foo" def foo_url, do: url(~p"/foo") def icon_url, do: url(~p"/images/icons/logo.png") + def font_url, do: url(~p"/fonts/inter.woff2") end assert StaticSegments.foo() == "/en/foo" assert StaticSegments.foo_url() == "https://example.com/en/foo" assert StaticSegments.icon_url() == "https://static.example.com/images/icons/logo.png" + assert StaticSegments.font_url() == "https://static.example.com/fonts/inter.woff2" end test "unverified_path" do From dd26a3e5300b5f69935c724e28ee6955bba91039 Mon Sep 17 00:00:00 2001 From: Steffen Deusch Date: Wed, 4 Nov 2026 16:47:59 +0100 Subject: [PATCH 4/4] Update CHANGELOG.md --- CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e04722ab15..87e0d54435 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,6 @@ ([#5509](https://github.com/phoenixframework/phoenix/issues/5509)) * [Phoenix.Socket] Type `topic` and `channel_pid` as possibly `nil` in `Phoenix.Socket.t`, as they are before a channel is joined - * [Phoenix.VerifiedRoutes] Only treat paths as static when their first - segment is one of the `:statics`, so routes such as `~p"/images-gallery"` - are verified and get the path prefixes - ([#6895](https://github.com/phoenixframework/phoenix/issues/6895)) ## Enhancements