Unexpected behavior with || operator with 'get_connect_params(socket)'

I am working on a Phoenix LiveView v.0.17.7 project based on a tutorial. In the app.js file, the user’s time zone is added to the params like this:

let liveSocket = new LiveSocket("/live", Socket, {
    params: {
        _csrf_token: csrfToken,
        timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
    }
})

On the server side, the time zone is pulled from the params and assigned to the socket:

def on_mount(:default, _params, _session, socket) do
    owner = Application.get_env(:calendex, :owner)
   time_zone = get_connect_params(socket)["timezone"] || owner.time_zone

    socket =
      socket
      |> assign(:time_zone, time_zone)
      |> assign(:owner, owner)

    IO.inspect(socket.assigns, label: "^^^^^^^ socket.assigns in init_assigns.ex")

    {:cont, socket}
  end

This results in a KeyError:

# key :time_zone not found in: %{name: "Bob Boss"}

Time_zone is not set as a parameter in ‘owner’ in config.sys, so this is not unexpected if time_zone is not being captured by get_connect_params(socket). However, if I change that statement to:

time_zone = get_connect_params(socket)["timezone"]

removing the || operator, then time_zone is assigned correctly, as shown by the inspect and in the terminal, in the parameters:

 [info] CONNECTED TO Phoenix.LiveView.Socket in 0µs
   Transport: :websocket
   Serializer: Phoenix.Socket.V2.JSONSerializer
   Parameters: %{"_csrf_token" => PUItOWRBBGoJPUMUUGcjThcxWxcdeiN7b0oH1w73cwzW1TWxAylaVJK9", "_mounts" => "0", "_track_static" => %{"0" => "http://localhost:4000/assets/app.css", "1" => "http://localhost:4000/assets/app.js"},
  "timezone" => "America/Chicago", "vsn" => "2.0.0"}

I can view the final app and it’s working, so the code provided using the || operator should work, but it doesn’t for me, which I cannot understand.

Any ideas why this won’t work?

Thanks! :smiley:

You’ve shown the timezone in the Parameters, but what does the inspect of assigns look like? Or what would be more helpful would be IO.inspect(time_zone, label: "time_zone"). I suspect that is nil (unless it’s false for some reason).

When time_zone is set correctly (without the || operator), the inspect looks like this:

^^^^^^^ socket.assigns in init_assigns.ex: %{
  __changed__: %{owner: true, time_zone: true},
  flash: %{},
  live_action: nil,
  owner: %{name: "Bob Boss"},
  time_zone: "America/Chicago"
}

When I include the operator as shown in the tutorial, I get the KeyError and the app doesn’t compile. This is what is shown in the terminal:

[info] GET /
[debug] Processing with Phoenix.LiveView.Plug.Elixir.CalendexWeb.PageLive/2
  Parameters: %{}
  Pipelines: [:browser]
[info] Sent 500 in 48ms
[error] #PID<0.640.0> running CalendexWeb.Endpoint (connection #PID<0.639.0>, stream id 1) terminated
Server: localhost:4000 (http)
Request: GET /
** (exit) an exception was raised:
    ** (KeyError) key :time_zone not found in: %{name: "Bob Boss"}

No parameters are passed.

Oh so the OR version works when you pass a timezone param but not when you don’t? I feel like that’s the behavior I would expect

When the first expression (getting timezone from params) is truthy, the || operator doesn’t bother to evaluate the second. But when the first expression is falsy, the second expression has to be evaluated.

iex(1)> "America/Chicago" || %{name: "Bob Boss"}.time_zone
"America/Chicago"
iex(2)> nil || %{name: "Bob Boss"}.time_zone
** (KeyError) key :time_zone not found in: %{name: "Bob Boss"}

So the second expression will always raise an exception, but it only gets evaluated when the first fails.

Or am I misunderstanding your question?

3 Likes

When the || is not present, the time zone is found and assigned as expected. It should also work that way when the || is included, but it doesn’t.

To put it another way, this should work:

time_zone = get_connect_params(socket)["timezone"] || owner.time_zone

But it doesn’t. I get the KeyError, since there is no time zone for owner. It acts as if the value returned from get_connect_params(socket)[“timezone”] is nil.

This gets the correct time zone and passes it to the view:

time_zone = get_connect_params(socket)["timezone"]

So if get_connect_params(socket) is working, why doesn’t first statement work?

BTW, using Elixir 1.13.3 and Phoenix 1.6.6.

Be aware that when the socket is not connected get_connect_params returns nil, and the Access behaviour on nil also returns nil.

iex> nil["timezone"]
nil

When you leave out the || owner.time_zone your disconnected @time_zone assign is nil until the socket connects and get_connect_params works.

3 Likes

I suspected something like this. But how do I deal with it, and why does it work for others?

Could you try wrapping it in a connected?/1 statement?

Something like:

...
if connected?(socket) do
  time_zone = get_connect_params(socket)["timezone"] || owner.time_zone
end
...

What tutorial are you following, I think I missed that?

Maybe you were supposed to include :time_zone in your :calendex config?

It looks like somewhere you may have this:

config :calendex, owner: %{name: "Bob Ross"}

And you may want it to look like this:

config :calendex, owner: %{name: "Bob Ross", time_zone: "America/Chicago"}
2 Likes

What do you mean by it works for others?

Now that you know that get_connect_params/1 is nil when the socket is disconnected, and you want to use || to set an alternative value, then you need to provide it a map/struct that actually has a time_zone key, or use something like || Map.get(owner, :time_zone, "some default"), otherwise you’ll keep seeing the error.

The recommendations from @f0rest8 and @brettbeatty are also good ways to deal with the error, but it’s difficult to help you further unless you tell us exactly what you’re trying to do.

1 Like

I’m following a tutorial and there is no mention of hard-coding a time zone value, as brettbeatty suggested. I looked at the repo for the project, and yes, the author did add the time zone as a property of the owner.

I did the same, hard-coding in my time zone. This fixed quite a few problems with the app.

This makes the app less useful, of course, since the time zone value won’t be customized for each user. I’ll contact the author and see if this is the intended behavior.

Thanks for the responses.

1 Like

I wrote a simple guide on one way to add the local time for each person. Not sure what the tutorial is that you’re following or what your specific use case is, but you may find something there… How to: Display Client’s Local Time in Your Elixir/Phoenix App (1.6+).

1 Like

I’ll take a look. Thanks.

1 Like