GET route interfere with POST

Hello, I am currently doing a phoenix JSON API, and I’ve met an uncommon problem.
Note that I used phx.gen.json for most of the code.

Here is my scope /api


scope "/api", WorklistWeb do
    pipe_through :api

    #get "/modalities/aet=:aet", ModalityController, :show #Break the POST somehow ?????
    # OK so after investigating. Using another atom than :show (ex :sow) and changing the function in controller works.
    # Meaning, the POST Route helper must try to find a controller with the :show atom, and doesn't pattern match.
    get "/examens/day=:day", ExamenController, :index

    resources "/patients", PatientController, except: [:new, :edit]
    resources "/modalities", ModalityController, except: [:new, :edit]
    resources "/examens", ExamenController, except: [:new, :edit]
  end

And the ModalityController parts which concern the problem

def create(conn, %{"modality" => modality_params}) do
    with {:ok, %Modality{} = modality} <- Modalities.create_modality(modality_params) do
      conn
      |> put_status(:created)
      |> put_resp_header("location", Routes.modality_path(conn, :show, modality))
      |> render("show.json", modality: modality)
    end
  end

  def show(conn, %{"id" => id}) do
    modality = Modalities.get_modality!(id)
    render(conn, "show.json", modality: modality)
  end

  def show(conn, %{"aet" => aet}) do
    modality = Modalities.get_modality_by_aet!(aet)
    render(conn, "show.json", modality: modality)
  end

So as I wrote in my scope comment, somehow the first route which is a pattern for get with URL arg, redirecting to the show function in the controller, broke my POST. This pattern work fine for GET, both with and without args. But it seem that « Routes.modality_path » from create match on my first GET route and it causes some error.

I found out that unit another atom that show for my first route seem to solve everything, but it doesn’t feel the right way, considering I could pattern match my GET just fine.

The error appear to be

[error] #PID<0.947.0> running WorklistWeb.Endpoint (connection #PID<0.922.0>, stream id 2) terminated
Server: localhost:4000 (http)
Request: POST /api/modalities
** (exit) an exception was raised:
    ** (ArgumentError) errors were found at the given arguments:

  * 1st argument: not a bitstring

This typically happens when calling Kernel.byte_size/1 with an invalid argument or when performing binary construction or binary concatenation with <> and one of the arguments is not a binary
        :erlang.byte_size(%Worklist.Modalities.Modality{__meta__: #Ecto.Schema.Metadata<:loaded, "modalities">, aet: "DICOMSTUFF1", id: 13, inserted_at: ~N[2022-03-08 16:48:33], modality: "DC", updated_at: ~N[2022-03-08 16:48:33]})
        (worklist 0.1.0) WorklistWeb.Router.Helpers.modality_path/4
        (worklist 0.1.0) lib/worklist_web/controllers/modality_controller.ex:18: WorklistWeb.ModalityController.create/2
        (worklist 0.1.0) lib/worklist_web/controllers/modality_controller.ex:1: WorklistWeb.ModalityController.action/2
        (worklist 0.1.0) lib/worklist_web/controllers/modality_controller.ex:1: WorklistWeb.ModalityController.phoenix_controller_pipeline/2
        (phoenix 1.6.6) lib/phoenix/router.ex:355: Phoenix.Router.__call__/2
        (worklist 0.1.0) lib/worklist_web/endpoint.ex:1: WorklistWeb.Endpoint.plug_builder_call/2
        (worklist 0.1.0) lib/plug/debugger.ex:136: WorklistWeb.Endpoint."call (overridable 3)"/2
        (worklist 0.1.0) lib/worklist_web/endpoint.ex:1: WorklistWeb.Endpoint.call/2
        (phoenix 1.6.6) lib/phoenix/endpoint/cowboy2_handler.ex:54: Phoenix.Endpoint.Cowboy2Handler.init/4
        (cowboy 2.9.0) /Users/clementauger/Desktop/Elixir/worklist/deps/cowboy/src/cowboy_handler.erl:37: :cowboy_handler.execute/2
        (cowboy 2.9.0) /Users/clementauger/Desktop/Elixir/worklist/deps/cowboy/src/cowboy_stream_h.erl:306: :cowboy_stream_h.execute/3
        (cowboy 2.9.0) /Users/clementauger/Desktop/Elixir/worklist/deps/cowboy/src/cowboy_stream_h.erl:295: :cowboy_stream_h.request_process/3
        (stdlib 3.17) proc_lib.erl:226: :proc_lib.init_p_do_apply/3

Any ideas ?

2 Likes

Routes.modality_path(conn, :show, modality)) - modality is a struct, there you are supposed to pass - modality.id ?

|> put_resp_header("location", Routes.modality_path(conn, :show, modality.id))
3 Likes

Apart from your error, I hope you don’t mind me pointing out a couple of things that stand out from your code.

This is an interesting way to generate a path with "day" parameter. Obviously it works for you and I don’t know if that’s part of your particular use case, but usually the path is written as "/examens/day/:day". This could be a problem because the helper ends up generating the path as "/examens/day%3Dmonday", encoding the = as %3D. Have you noticed this in your HTML?

In your example the routes helpers end up with the same name, examen_path.

examen_path GET /examens/day=:day   WorklistWeb.ExamenController :index
examen_path GET /examens            WorklistWeb.ExamenController :index

Giving the GET path a named helper is usually the right way to separate concerns between a specific path from the ones generated by resources, and makes it much easier for others reading the code to understand the distinction.

get "/examens/day=:day", ExamenController, :index, as: :examens_day

results in

examens_day_path GET /examens/day=:day  WorklistWeb.ExamenController :index

and you can call Routes.examens_day_path(@conn, :index, "monday")

3 Likes

Many thanks to everyone, I managed to make it work.

1 Like

Just in case someone use this topic later I want to point that this (arguably) bad design pattern was due to my misunderstanding of the phoenix framework. What I tried to archieve was url args, but as I didn’t knew much about Phoenix, I went astray.

The good way is to use parametric pattern matching in the controller, the framework handle the rest.
Exemple with two arguments:

URL : http://localhost:4000/api/examens?day=08032022&tag=TAG01

router : resources "/examens", ExamenController, except: [:new, :edit]

controller pattern : def index(conn, %{"day" => day, "tag" => tag}) do
1 Like