Got assign/3 expects a socket error on assign/2 or assign/3 when trying to update assigns in function

good Morning, I have a function that place a request to an elasticsearch REST API of a library management software, and I am trying to manipulate the response so I get the results(so far all OK), and the total results quantity(using a KErnel.tap/2 function for this). the response manipulation is as follows:

raw_response.body
    |> Jason.decode!()
    |> Map.fetch("hits")
    |> elem(1)
    |> tap(&get_total_results(&1, socket ))
    |> Map.fetch("hits")
    |> elem(1)

I use Kernel.tap/2 since the results is “hidden” in an upper structure of the response.

the get_total_results function is as follows:

@impl true
  def get_total_results(tr,socket) do
    total_res =
      tr["total"]
      |> tap(&IO.inspect(&1))

    socket =
      socket
      |> assign(results_qty: total_res)
  end

as soon as I use this function I get this error:

** (ArgumentError) assign/3 expects a socket from Phoenix.LiveView/Phoenix.LiveComponent  or an assigns map from Phoenix.Component as first argument, got: nil.

I have read the docs but have not got anyhing clear about this, I am using assigns update in otehr parts of this module and all is OK, I thought the problem was the socket id, but is the same that I use in mount/3 in the beggining of the code, the mount/3 function is this one.

  use DedalosPhoenixWeb, :live_view
  use Phoenix.HTML

  alias DedalosPhoenixWeb.{OpacLive, NavBarLive}

  def mount(params, _session, socket) do
    {:ok,
     assign(socket,
       page_number: 1,
       page_size: 10,
       results_qty: 20,
       query_params: params,
       filtered_results: []
     )}
  end

Please anyone could clear this for me, I am still readeing the official docs to see If I get something clear, thanks in advance.

Can you show us the function where you get the raw_response? I see you pass in socket but do not know where it comes from.

Here is the complete function

@impl true
  def process_request(terms, socket) do
    index_terms = build_elastic_index(terms, socket)

    uri_q =
      terms
      |> Enum.reject(fn {k, _v} -> k == index_terms.filter_term end)
      |> Enum.map(fn x -> elem(x, 0) <> ": " <> elem(x, 1) <> " " end)
      |> Enum.intersperse(terms[index_terms.filter_term] <> " ")
      |> to_string()

    p_body = %{query: %{query_string: %{query: uri_q}}}
    body = Jason.encode(p_body) |> elem(1)
    body

    raw_response =
      Tesla.post!(
        'http://biblioteca.ccpadrevarela.org:19200/#{index_terms.search_index}/_search?size=10000',
        body,
        headers: ["User-Agent": "Dedalos", "Content-Type": "application/json"]
      )

    raw_response.body
    |> Jason.decode!()
    |> Map.fetch("hits")
    |> elem(1)
    |> tap(&get_total_results(&1, socket ))
    |> Map.fetch("hits")
    |> elem(1)
  end

also the build_elastic_function/2 function is here:

  @impl true
  def build_elastic_index(terms, socket) do
    if terms |> Map.has_key?("QUERY_OPERATOR") do
      %{filter_term: "QUERY_OPERATOR", search_index: "_all"}
    else
      %{filter_term: "choice", search_index: terms["choice"]}
    end
  end

If you IO.inspect the socket right before you use the assign/3? Does it return nil?
As a side note, whatever you assign to the socket in get_total_results/2 won’t be persisted as you do not return the updated socket.

This code will likely not do what you think it’s doing. The result from get_total_result will be discarded by tap. tap is used to perform side effects and returns its input unchanged (in this case, the result of elem(1)).
You can add a dbg() statement at the end of the pipeline and you’ll see what I mean/

1 Like

A general style note - Map.fetch + elem is going to crash if the specified key is missing (since fetch returns a bare :error), but not with a particularly clear error message:

iex(1)> elem(:error, 1)
** (ArgumentError) errors were found at the given arguments:

  * 2nd argument: not a tuple

    :erlang.element(2, :error)
    iex:1: (file)

Prefer using Map.fetch! which will also crash, but with a message that clearly states what wasn’t found:

iex(2)> Map.fetch!(%{}, :not_there)
** (KeyError) key :not_there not found in: %{}
    (stdlib 4.1.1) :maps.get(:not_there, %{})
    iex:2: (file)

As others have already pointed out in this thread, you’ll want to return that modified socket from process_request if you expect the modified assigns to stick around.

But there’s a bigger design idea at the heart: keeping “socket manipulation” code separate from “data manipulation” code. A function like get_total_results is trying to do two unrelated things and making both of them harder:

  • get the total from the results
  • put it into the assigns

Separating those, even if just into separate functions in the same module, can make it easier to see what’s going on (and avoid tricky “how do I return socket from here?” conundrums):

  hits =
    raw_response.body
    |> Jason.decode!()
    |> Map.fetch!("hits")

  socket =
    socket
    |> assign(:results_qty, hits["total"])
    |> assign(:results, hits)

I’m guessing on that last one, but it seemed logical that you’d want the results to go with the count in the assigns…

1 Like

I could be wrong but I think it is better to use MyAppWeb, :html for functional components ( or :component for 1.6) and :live_component.

One hint is that they have the Phoenix.HTML module in a separate section for modules that are “under the hood”.

Also if you look at the documentation Phoenix.HTML is imported in a private function.

Maybe consider separating your LiveView and component(s) into different modules.

that was my intent to create a side effect using the calling to the function get_total_results and in the body of that function is where i’ d like to update the assigns. I am seeing now as tomkonidas stated that when I put IO.inspect(socket) right before the assign/3 returns nil.

when I do that it returns this,

#Phoenix.LiveView.Socket<
  id: "phx-F06CX3tRkdalVgCF",
  endpoint: DedalosPhoenixWeb.Endpoint,
  view: DedalosPhoenixWeb.ResultsTableLive,
  parent_pid: nil,
  root_pid: #PID<0.1511.0>,
  router: DedalosPhoenixWeb.Router,
  assigns: %{
    __changed__: %{
      filtered_results: true,
      page_number: true,
      page_size: true,
      query_params: true,
      results_qty: true
    },
    filtered_results: [],
    flash: %{},
    live_action: nil,
    page_number: 1,
    page_size: 10,
    query_params: %{"QUERY_OPERATOR" => "OR", "tĂ­tulo" => "amelia"},
    results_qty: 20
  },
  transport_pid: #PID<0.1506.0>,
  ...
>
[debug] Replied in 10ms
nil

I think you could do something like this.

raw_response.body
|> Jason.decode!()
|> Map.fetch(“hits”)
|> elem(1)
|> then( fn elem →
elem |> tap(&get_total_results(&1, socket ))
next = elem |> Map.fetch(“hits”)
next
end )
|> elem(1)