Skip to content

Commit 0079ad0

Browse files
committed
Allow forwarding to the same Plug more than once, as long as it is not a router
1 parent 98ab9a7 commit 0079ad0

File tree

6 files changed

+288
-116
lines changed

6 files changed

+288
-116
lines changed

lib/phoenix/router.ex

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,6 @@ defmodule Phoenix.Router do
364364
defp prelude(opts) do
365365
quote do
366366
Module.register_attribute(__MODULE__, :phoenix_routes, accumulate: true)
367-
@phoenix_forwards %{}
368367
# TODO: Require :helpers to be explicit given
369368
@phoenix_helpers Keyword.get(unquote(opts), :helpers, true)
370369

@@ -541,8 +540,7 @@ defmodule Phoenix.Router do
541540
@doc false
542541
defmacro __before_compile__(env) do
543542
routes = env.module |> Module.get_attribute(:phoenix_routes) |> Enum.reverse()
544-
forwards = env.module |> Module.get_attribute(:phoenix_forwards)
545-
routes_with_exprs = Enum.map(routes, &{&1, Route.exprs(&1, forwards)})
543+
routes_with_exprs = Enum.map(routes, &{&1, Route.exprs(&1)})
546544

547545
helpers =
548546
if Module.get_attribute(env.module, :phoenix_helpers) do
@@ -578,13 +576,6 @@ defmodule Phoenix.Router do
578576
end
579577
end
580578

581-
forwards =
582-
for {plug, script_name} <- forwards do
583-
quote do
584-
def __forward__(unquote(plug)), do: unquote(script_name)
585-
end
586-
end
587-
588579
forward_catch_all =
589580
quote generated: true do
590581
@doc false
@@ -623,26 +614,32 @@ defmodule Phoenix.Router do
623614
unquote(verify_catch_all)
624615
unquote(matches)
625616
unquote(match_catch_all)
626-
unquote(forwards)
627617
unquote(forward_catch_all)
628618
end
629619
end
630620

631621
defp build_verify(path, routes_per_path) do
632622
routes = Map.get(routes_per_path, path)
623+
warn_on_verify? = Enum.all?(routes, & &1.warn_on_verify?)
633624

634-
forward_plug =
635-
Enum.find_value(routes, fn
636-
%{kind: :forward, plug: plug} -> plug
637-
_ -> nil
638-
end)
625+
case Enum.find(routes, &(&1.kind == :forward)) do
626+
%{metadata: %{forward: forward}, plug: plug} ->
627+
quote generated: true do
628+
def __forward__(unquote(plug)) do
629+
unquote(forward)
630+
end
639631

640-
warn_on_verify? = Enum.all?(routes, & &1.warn_on_verify?)
632+
def __verify_route__(unquote(path)) do
633+
{{unquote(plug), unquote(forward)}, unquote(warn_on_verify?)}
634+
end
635+
end
641636

642-
quote generated: true do
643-
def __verify_route__(unquote(path)) do
644-
{unquote(forward_plug), unquote(warn_on_verify?)}
645-
end
637+
_ ->
638+
quote generated: true do
639+
def __verify_route__(unquote(path)) do
640+
{nil, unquote(warn_on_verify?)}
641+
end
642+
end
646643
end
647644
end
648645

@@ -1199,16 +1196,25 @@ defmodule Phoenix.Router do
11991196
@doc """
12001197
Forwards a request at the given path to a plug.
12011198
1202-
All paths that match the forwarded prefix will be sent to
1203-
the forwarded plug. This is useful for sharing a router between
1199+
This is commonly used to forward all subroutes to another Plug.
1200+
For example:
1201+
1202+
forward "/admin", SomeLib.AdminDashboard
1203+
1204+
The above will allow `SomeLib.AdminDashboard` to handle `/admin`,
1205+
`/admin/foo`, `/admin/bar/baz`, and so on. Furthermore,
1206+
`SomeLib.AdminDashboard` does not to be aware of the prefix it
1207+
is mounted in. From its point of view, the routes above are simply
1208+
handled as `/`, `/foo`, and `/bar/baz`.
1209+
1210+
A common use case for `forward` is for sharing a router between
12041211
applications or even breaking a big router into smaller ones.
1212+
However, in other for route generation to route accordingly, you
1213+
can only forward to a given `Phoenix.Router` once.
1214+
12051215
The router pipelines will be invoked prior to forwarding the
12061216
connection.
12071217
1208-
However, we don't advise forwarding to another endpoint.
1209-
The reason is that plugs defined by your app and the forwarded
1210-
endpoint would be invoked twice, which may lead to errors.
1211-
12121218
## Examples
12131219
12141220
scope "/", MyApp do

lib/phoenix/router/route.ex

Lines changed: 86 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ defmodule Phoenix.Router.Route do
1111
1212
* `:verb` - the HTTP verb as an atom
1313
* `:line` - the line the route was defined
14-
* `:kind` - the kind of route, one of `:match`, `:forward`
14+
* `:kind` - the kind of route, either `:match` or `:forward`
1515
* `:path` - the normalized path as string
1616
* `:hosts` - the list of request hosts or host prefixes
1717
* `:plug` - the plug module
@@ -25,9 +25,22 @@ defmodule Phoenix.Router.Route do
2525
* `:warn_on_verify?` - whether or not to warn on route verification
2626
"""
2727

28-
defstruct [:verb, :line, :kind, :path, :hosts, :plug, :plug_opts,
29-
:helper, :private, :pipe_through, :assigns, :metadata,
30-
:trailing_slash?, :warn_on_verify?]
28+
defstruct [
29+
:verb,
30+
:line,
31+
:kind,
32+
:path,
33+
:hosts,
34+
:plug,
35+
:plug_opts,
36+
:helper,
37+
:private,
38+
:pipe_through,
39+
:assigns,
40+
:metadata,
41+
:trailing_slash?,
42+
:warn_on_verify?
43+
]
3144

3245
@type t :: %Route{}
3346

@@ -47,29 +60,71 @@ defmodule Phoenix.Router.Route do
4760
Receives the verb, path, plug, options and helper
4861
and returns a `Phoenix.Router.Route` struct.
4962
"""
50-
@spec build(non_neg_integer, :match | :forward, atom, String.t, String.t | nil, atom, atom, atom | nil, list(atom), map, map, map, boolean, boolean) :: t
51-
def build(line, kind, verb, path, hosts, plug, plug_opts, helper, pipe_through, private, assigns, metadata, trailing_slash?, warn_on_verify?)
63+
@spec build(
64+
non_neg_integer,
65+
:match | :forward,
66+
atom,
67+
String.t(),
68+
String.t() | nil,
69+
atom,
70+
atom,
71+
atom | nil,
72+
list(atom),
73+
map,
74+
map,
75+
map,
76+
boolean,
77+
boolean
78+
) :: t
79+
def build(
80+
line,
81+
kind,
82+
verb,
83+
path,
84+
hosts,
85+
plug,
86+
plug_opts,
87+
helper,
88+
pipe_through,
89+
private,
90+
assigns,
91+
metadata,
92+
trailing_slash?,
93+
warn_on_verify?
94+
)
5295
when is_atom(verb) and is_list(hosts) and
53-
is_atom(plug) and (is_binary(helper) or is_nil(helper)) and
54-
is_list(pipe_through) and is_map(private) and is_map(assigns) and
55-
is_map(metadata) and kind in [:match, :forward] and
56-
is_boolean(trailing_slash?) do
57-
%Route{kind: kind, verb: verb, path: path, hosts: hosts, private: private,
58-
plug: plug, plug_opts: plug_opts, helper: helper,
59-
pipe_through: pipe_through, assigns: assigns, line: line, metadata: metadata,
60-
trailing_slash?: trailing_slash?, warn_on_verify?: warn_on_verify?}
96+
is_atom(plug) and (is_binary(helper) or is_nil(helper)) and
97+
is_list(pipe_through) and is_map(private) and is_map(assigns) and
98+
is_map(metadata) and kind in [:match, :forward] and
99+
is_boolean(trailing_slash?) do
100+
%Route{
101+
kind: kind,
102+
verb: verb,
103+
path: path,
104+
hosts: hosts,
105+
private: private,
106+
plug: plug,
107+
plug_opts: plug_opts,
108+
helper: helper,
109+
pipe_through: pipe_through,
110+
assigns: assigns,
111+
line: line,
112+
metadata: metadata,
113+
trailing_slash?: trailing_slash?,
114+
warn_on_verify?: warn_on_verify?
115+
}
61116
end
62117

63118
@doc """
64119
Builds the compiled expressions used by the route.
65120
"""
66-
def exprs(route, forwards) do
121+
def exprs(route) do
67122
{path, binding} = build_path_and_binding(route)
68123

69124
%{
70125
path: path,
71126
binding: binding,
72-
dispatch: build_dispatch(route, forwards),
127+
dispatch: build_dispatch(route),
73128
hosts: build_host_match(route.hosts),
74129
path_params: build_path_params(binding),
75130
prepare: build_prepare(route),
@@ -78,6 +133,7 @@ defmodule Phoenix.Router.Route do
78133
end
79134

80135
def build_host_match([]), do: [Plug.Router.Utils.build_host_match(nil)]
136+
81137
def build_host_match([_ | _] = hosts) do
82138
for host <- hosts, do: Plug.Router.Utils.build_host_match(host)
83139
end
@@ -91,7 +147,7 @@ defmodule Phoenix.Router.Route do
91147
{_params, segments} =
92148
case route.kind do
93149
:forward -> Plug.Router.Utils.build_path_match(path <> "/*_forward_path_info")
94-
:match -> Plug.Router.Utils.build_path_match(path)
150+
:match -> Plug.Router.Utils.build_path_match(path)
95151
end
96152

97153
rewrite_segments(segments)
@@ -101,7 +157,8 @@ defmodule Phoenix.Router.Route do
101157
defp rewrite_segments(segments) do
102158
{segments, {binding, _counter}} =
103159
Macro.prewalk(segments, {[], 0}, fn
104-
{name, _meta, nil}, {binding, counter} when is_atom(name) and name != :_forward_path_info ->
160+
{name, _meta, nil}, {binding, counter}
161+
when is_atom(name) and name != :_forward_path_info ->
105162
var = Macro.var(:"arg#{counter}", __MODULE__)
106163
{var, {[{Atom.to_string(name), var} | binding], counter + 1}}
107164

@@ -140,27 +197,32 @@ defmodule Phoenix.Router.Route do
140197
{[{key, var}], [{key, merge}]}
141198
end
142199

143-
defp build_dispatch(%Route{kind: :match, plug: plug, plug_opts: plug_opts}, _forwards) do
200+
defp build_dispatch(%Route{kind: :match, plug: plug, plug_opts: plug_opts}) do
144201
quote do
145202
{unquote(plug), unquote(Macro.escape(plug_opts))}
146203
end
147204
end
148205

149-
defp build_dispatch(%Route{kind: :forward, plug: plug, plug_opts: plug_opts}, forwards) do
150-
segments = Map.fetch!(forwards, plug)
151-
206+
defp build_dispatch(%Route{
207+
kind: :forward,
208+
plug: plug,
209+
plug_opts: plug_opts,
210+
metadata: metadata
211+
}) do
152212
quote do
153213
{
154214
Phoenix.Router.Route,
155-
{unquote(segments), unquote(plug), unquote(Macro.escape(plug_opts))}
215+
{unquote(metadata.forward), unquote(plug), unquote(Macro.escape(plug_opts))}
156216
}
157217
end
158218
end
159219

160220
defp build_params() do
161221
params = Macro.var(:params, :conn)
162222
path_params = Macro.var(:path_params, :conn)
163-
merge_params = quote(do: Phoenix.Router.Route.merge_params(unquote(params), unquote(path_params)))
223+
224+
merge_params =
225+
quote(do: Phoenix.Router.Route.merge_params(unquote(params), unquote(path_params)))
164226

165227
{
166228
[{:params, params}],

lib/phoenix/router/scope.ex

Lines changed: 15 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,12 @@ defmodule Phoenix.Router.Scope do
5454
|> Keyword.get(:metadata, %{})
5555
|> Map.put(:log, Keyword.get(opts, :log, top.log))
5656

57-
if kind == :forward do
58-
register_forwards(module, path, plug)
59-
end
57+
metadata =
58+
if kind == :forward do
59+
Map.put(metadata, :forward, validate_forward!(path, plug))
60+
else
61+
metadata
62+
end
6063

6164
Phoenix.Router.Route.build(
6265
line,
@@ -76,31 +79,18 @@ defmodule Phoenix.Router.Scope do
7679
)
7780
end
7881

79-
defp register_forwards(module, path, plug) when is_atom(plug) do
80-
plug = expand_alias(module, plug)
81-
phoenix_forwards = Module.get_attribute(module, :phoenix_forwards)
82-
83-
path_segments =
84-
case Plug.Router.Utils.build_path_match(path) do
85-
{[], path_segments} ->
86-
if phoenix_forwards[plug] do
87-
raise ArgumentError,
88-
"#{inspect(plug)} has already been forwarded to. A module can only be forwarded a single time"
89-
end
82+
defp validate_forward!(path, plug) when is_atom(plug) do
83+
case Plug.Router.Utils.build_path_match(path) do
84+
{[], path_segments} ->
85+
path_segments
9086

91-
path_segments
92-
93-
_ ->
94-
raise ArgumentError,
95-
"dynamic segment \"#{path}\" not allowed when forwarding. Use a static path instead"
96-
end
97-
98-
phoenix_forwards = Map.put(phoenix_forwards, plug, path_segments)
99-
Module.put_attribute(module, :phoenix_forwards, phoenix_forwards)
100-
plug
87+
_ ->
88+
raise ArgumentError,
89+
"dynamic segment \"#{path}\" not allowed when forwarding. Use a static path instead"
90+
end
10191
end
10292

103-
defp register_forwards(_, _, plug) do
93+
defp validate_forward!(_, plug) do
10494
raise ArgumentError, "forward expects a module as the second argument, #{inspect(plug)} given"
10595
end
10696

lib/phoenix/verified_routes.ex

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -757,19 +757,21 @@ defmodule Phoenix.VerifiedRoutes do
757757

758758
defp match_route?(router, split_path) when is_list(split_path) do
759759
case router.__verify_route__(split_path) do
760-
{_forward_plug, true = _warn_on_verify?} -> false
761-
{nil = _forward_plug, false = _warn_on_verify?} -> true
762-
{fwd_plug, false = _warn_on_verify?} -> match_forward_route?(router, fwd_plug, split_path)
763-
:error -> false
764-
end
765-
end
760+
{_forward_plug, true = _warn_on_verify?} ->
761+
false
766762

767-
defp match_forward_route?(router, forward_router, split_path) do
768-
if function_exported?(forward_router, :__routes__, 0) do
769-
script_name = router.__forward__(forward_router)
770-
match_route?(forward_router, split_path -- script_name)
771-
else
772-
true
763+
{nil = _forward_plug, false = _warn_on_verify?} ->
764+
true
765+
766+
{{router, script_name}, false = _warn_on_verify?} ->
767+
if function_exported?(router, :__routes__, 0) do
768+
match_route?(router, split_path -- script_name)
769+
else
770+
true
771+
end
772+
773+
:error ->
774+
false
773775
end
774776
end
775777

0 commit comments

Comments
 (0)