TL;DR I added a /foo/:name
so I can access by name but param is catching actual routes, and vice versa.
But when I do /foo/new
this is treated like the /foo/:name
param and goes to :show
(which is normally /foo/:id
) rather than the proper /foo/new
, and causes an error.
[info] GET /foo/new
[debug] Processing with MyApp.FooController.show/2
Parameters: %{"name" => "new"}
Pipelines: [:browser]
It’s getting caught by "/foo/:name", FooController, :show
, which is above the default resources "/foo", FooController
in file. Reordering doesn’t solve this since then /foo/:name
gets caught by /foo/:id
inside resources.
How can I make this work? I want to exclude the word new
from being a param. Is there some kind of next()
function so I could pass on the request when it’s rejected?
I don’t want to change id
to name
for all /foo
routes, which is the only option I can find.
This solves it temporarily, but then I cannot access by id:
resources "/organizations", OrganizationController, except: [:show]
There’s not. Routes are matched with pattern matching, which matches the first item. You’d need to decompose resources
into its individual routes and solve things by reordering after that.
1 Like
Ahh okay, so then maybe I cannot use resources here. I’ll try breaking it up.
You must consider that /foos/:name
and /foos/:id
is essentially the same route - just that the name of the bound parameter is different regardless of if it’s an ID or a name. If you want to be able to fetch Foo
resources by name or ID, you will have to do that inside the controller - treat the incoming parameter as id_or_name
and write your lookup logic accordingly.
There’s nothing wrong per se with using resources
here, just that the generated route clause for show
will have the path parameter named "id"
by default. You could leave that as it is, and just treat it as id_or_name
in the controller: def show(conn, %{"id" => id_or_name}) do ...
.
Update: maybe to expand a bit on the parameter names in the route definitions, in case that’s the source of the confusion: the router does not “know” about the fields of your resources, the fact that Foo
has a :name
field does not matter. You could define a route with get "/foos/:whatever", FooController, :show
- this will only affect the name of the parameter passed to the controller: def show(conn, %{"whatever" => value}) do ...
.
3 Likes
These helped me solve the problem. I thought I’d need a middleware-type thing, like next()
but I didn’t. I was over-complicating it. Once I got the routing right, it works. Here’s a psuedo-codey version of what I did.
Controller
def show(conn, %{"param" => param}) do
# if param is ID
if is_digit(param) do
# query ecto by ID with Repo.get! inside
looked_up_obj = MyObj.get_obj!(param)
#redirect to show route so rest param uses name slug in URL, not ID
redirect(conn, to: "/foo/#{looked_up_obj.slug}")
else # is slugged slug name as param
#query ecto by name using Repo.get_by! inside
looked_up_obj = MyObj.get_obj_by_name!(param)
render(conn, "show.html", organization: organization)
end
end
Router.ex
scope "/", MyAppWeb do
pipe_through :browser
resources "/foo", FooController, except: [:show]
get "/foo/:param", FooController, :show
end
Thanks alot for the help!
1 Like