Rest param is being treated as route

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