Order of params in generated GET URLs changed after Elixir & Erlang updates

I have updated Erlang from 24 to 26, and Elixir from 1.14 to 1.16 and now a lot of my tests are broken, because URL generation in Phoenix/Elixir seems to be working somewhat differently.

My tests assume I am being redirected to URLs, for example I get this error:

** (ArgumentError) expected LiveView to redirect to "/shipments?period=yesterday&status=shipped&tenant_id=", but got "/shipments?status=shipped&period=yesterday&tenant_id="

There is no problem from the functional point of view, the params are still accepted as they are but in the generated URLs they now have a different order.

I think previously, the order was alphabetical, so I had “period” followed by “status” and then “tenant”, and now it’s somewhat random, with “status”, “period” and “tenant” in this example, but in other places it’s different.

Is there a way I can force the previous behavior?

Edit: this seems to be due to Erlang 24 to 26 update

This seems to be due to this change in Erlang:

The documentation states that:

“The new order is undefined and may change between different invocations of the Erlang VM”

So I guess we should not rely on the order of parameters being stable / predictable, so just updating the order in tests may not do the job

The fix seems to be to use router helpers in my tests, but I think I would like still to re-introduce alphabetical ordering for params for the sake of consistency and not breaking things for the integrations the system works with.

Any ideas how to achieve the previous behavior?

There’s a new iterator in OTP to iterate over a map in stable order. But that one needs to be explicitly used.

How are you generating your query urls?

If you’re interpolating a map into sigil_p, you could sort it into a Keyword list.

# shipments_live.ex
params = %{status: :shipped, period:, :yesterday, tenant_id: 1}
assign(socket, :shipment_params, Enum.sort(params))

# shipments_live.html.heex
# @shipment_params == [period: :yesterday, status: :shipped, tenant_id: 1]
# <a href="/shipments?period=yesterday&status=shipped&tenant_id=1" ...></a>
<.link navigate={~p"/shipments?#{@shipment_params}"}><./link>

Which also works for URIs

query = 
  %{status: :shipped, period:, :yesterday, tenant_id: 1}
  |> Enum.sort()
  |> URI.encode_query()

uri = 
  "/shipments"
  |> URI.new!()
  |> URI.append_query(query)

Keyword lists are great when I want to control the semantic ordering so users can infer what the link is likely to do based on some domain knowledge.

However in tests, I prefer to test query params with map’s structural comparison:

{:ok, view, _html} = live(conn, ~p"/shipments")

expected_query = %{"period" => "yesterday", "status" => "shipped", "tenant_id" => "1"}

result = view |> element(link_selector) |> render_click()
# where link_selector = "a#known-id", or something more fancy like 
# "a[href*='period=yesterday'][href*='status=shipped'][href*='tenant_id=1']"

{new_uri, _flash} = assert_redirect(view)
redirected_query = new_uri |> URI.new!() |> Map.get(:query, "") |> URI.decode_query()

assert expected_query == redirected_query
1 Like

Correct me if I am missing something uber obvious but can’t you just URI.decode_query or Plug.Conn.Query.decode the URI and just compare maps? :thinking:

3 Likes