Phoenix 1.7 upgrade - error: key :conn not found in: %{

Hello,

I have a problem after logging in to my app and I don’t know how to solve this error: key :conn not found in: %{__changed__: % in my_app_web/templates/layout/app.html.heex Has anyone encountered this before? I’m upgrading from phoenix from 1.6.9 to 1.7.0 and phoenix_live_view from 17.8 to 18.3.
I don’t know what I should have set wrong. I followed the upgrade instructions or also the video from Elixircast

I checked web.ex and everything from tutorials, but it seems ok.

Has anyone had the same problem please?

Any ideas? Thank you.


Need more information to solve a problem?

2 Likes

I think you want to use a root layout with LV to put everything outside of the html body. Usually that’s in root.html.heex.

@LostKobrakai I’am so sorry, but But I don’t fully understand the answer. Can you give me some more details please?

More info:

I have 90% on the project without LV. Normally through the Controller. In general, I solve the problem anywhere after the user logs into the application. Everything was fine before the upgrade.

I don’t have root.html because it’s quite an old project and there are a lot of other little things in it. I only have /app.html which looks like this:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title><%= assigns[:page_title] || "ABC" %></title>
    <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
    <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
    <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
    <meta name="msapplication-TileColor" content="#ffffff" />
    <meta name="theme-color" content="#ffffff" />
    <link rel="stylesheet" href={Routes.static_path(@conn, "/css/app.css")} />
    <link
      href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap"
      rel="stylesheet"
    />
    <%= csrf_meta_tag() %>
    <%= countly_header() %>
  </head>
  <body class="h-100">
    <header class="mb-3 fixed-top" id="loader">
      <%= render("navbar.html", conn: @conn, current_user: @current_user) %>
    </header>

    <main role="main" class="container" style="margin-top: 70px">
      <%= if Phoenix.Flash.get(@flash, :info) do %>
        <p class="shadow-sm alert alert-info mb-4" role="alert">
          <i class="far fa-info-circle mr-1"></i> <%= Phoenix.Flash.get(@flash, :info) %>
        </p>
      <% end %>

      <%= if Phoenix.Flash.get(@flash, :error) do %>
        <p class="shadow-sm alert alert-danger mb-4" role="alert">
          <i class="far fa-exclamation-triangle mr-1"></i> <%= Phoenix.Flash.get(@flash, :error) %>
        </p>
      <% end %>

      <%= if Phoenix.Flash.get(@flash, :warn) do %>
        <p class="shadow-sm alert alert-warning mb-4" role="alert">
          <i class="far fa-exclamation-triangle mr-1"></i> <%= Phoenix.Flash.get(@flash, :warn) %>
        </p>
      <% end %>

      <%= @inner_content %>
    </main>
    <script type="text/javascript" src={Routes.static_path(@conn, "/js/app.js")}>
    </script>
    <%= raw(@conn.assigns[:intercom]) %>
    <footer>
      <div class="container">
        <div class="position-sticky text-center">
          <div class="logos">
            <a href="https://abc.cz//">
              <img
                src={Routes.static_url(@conn, "/images/ABC.png")}
                alt=""
                style="height: 30px;"
              />
            </a>
            <a href="https://abc.cz//">
              <img
                src={Routes.static_url(@conn, "/images/ABC.png")}
                alt=""
                style="height: 30px;"
              />
            </a>
            <div class="position-sticky text-center">
              <p><%= gettext("Developed by") %></p>
              <a href="https://abc.cz//">
                <img src={Routes.static_url(@conn, "/images/ABC.png")} alt="" />
              </a>
            </div>
          </div>
          <span class="footer-version">v<%= app_version() %></span>
        </div>
      </div>
    </footer>
    <%= countly_footer() %>
  </body>
</html>

in my MyAppWeb.ex:

  def controller do
    quote do
      use Phoenix.Controller,
        namespace: MyAppWeb
      alias MyAppWeb.Router.Helpers, as: Routes
      import Plug.Conn
      import MyAppWeb.Gettext
      unquote(verified_routes())

      action_fallback MyAppWeb.FallbackController
    end
  end
  def live_view do
    quote do
      #use Phoenix.LiveView, layout: {MyAppWeb.LayoutView, "app.html"}
      use Phoenix.LiveView, layout: {MyAppWeb.LayoutView, :app}
      alias MyAppWeb.Router.Helpers, as: Routes
      import Appsignal.Phoenix.LiveView, only: [live_view_action: 4]

      unquote(html_helpers())
      unquote(view_helpers())
    end
  end

/router.ex

  pipeline :protected do
    plug Pow.Plug.RequireAuthenticated, error_handler: Pow.Phoenix.PlugErrorHandler
    plug(:put_root_layout, {MyAppWeb.LayoutView, :app})
  end

If you’re using LV you should have a root.html.heex and use that as root template. Leave just the pieces within <body> in app.html.heex, there rest goes into root.html.heex. Everything in the root template is not touched by LV, but everything within app.html can be. LV trying to manage head content however won’t go well. root.html.heex will always receive a @conn, but (if you stick to using it for LV and non-LV) app.html.heex will either have access to a @conn or @socket depending on the type of page. For all things related to links you can substitute them for MyAppWeb.Endpoint, which works for both types of pages.

2 Likes

I rewrite app.html.heex to root.html.heex +app.html.heex+ live.html.heex and it help me a lot on most forms. But when I have live_render function in form (html.heex or controller), I have same error. I tried to replace Phoenix.LiveView.Controller to Phoenix.Component but it didn’t help me.

I’m missing the point about the endpoint. Sorry for the misunderstanding. :smiley:

example:

  def index(conn, params) do
    Phoenix.LiveView.Controller.live_render(conn, MessageLive,
      session: %{
        "user" => conn.assigns.current_user,
        "thread_id" => Map.get(params, "t")
      }
    )
  end

Not answering your specific question here, but generally I find the best approach is to use the generator to create two new empty projects, one in the old version and one in the new version and use a diff of those two as a guide for what to change in your project.

I’ve tried that before, but to no avail.

Just to give you an idea.
The project I’m making changes to is originally based in 2020. Nothing has been done on it for a year. LiveView was handled very differently than it is today. I’m the only one working on the project. I only have an external senior mentor who is currently too busy. And my experience with Elixir is half a year. As a Junior, I’m definitely missing out on a lot of things, but I need to address specific issues because I’m currently stuck after the new year and I’m starting to feel like I’m not keeping up. :smiley:

Right, yes, that’s a difficult situation. What I think the problem here is is that you have @conn inside your heex template tags ({}), which is then being processed by LV as a tracked template var, and hence it’s looking for the :conn key in its internal __changed__ map (which it can’t find). If you look in a new 1.7 project’s root.html.heex you’ll see it’s now using the verified routes ~p sigil:

<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />

…so that should solve this problem, but I fear that’ll just lead you to the next, and the next, and the next.

1 Like

You are absolutely right. It took me to the next one, specifically
<%= render("navbar.html", conn: @conn, current_user: @current_user) %>
which is visible on the original app.html.heex added here. So I would have to rewrite everything, if I understand it correctly.

I’m adding Endpoint because I don’t understand how Benjamin meant it.

defmodule MyWebApp.Endpoint do
  use Phoenix.Endpoint, otp_app: :my_app
  use Appsignal.Phoenix

  # The session will be stored in the cookie and signed,
  # this means its contents can be read but not tampered with.
  # Set :encryption_salt if you would also like to encrypt it.
  # hibernate_after - wait 45 seconds before end connection (heroku timeout is 55 sec)
  @session_options [
    store: :cookie,
    key_iterations: 500,
    key: "_my_app_key",
    signing_salt: Application.compile_env!(:my_app, :signing_salt),
    hibernate_after: 45_000
  ]

  socket "/socket", MyWebApp.UserSocket,
    websocket: true,
    longpoll: false

  socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]

  # Serve at "/" the static files from "priv/static" directory.
  #
  # You should set gzip to true if you are running phx.digest
  # when deploying your static files in production.
  plug Plug.Static,
    at: "/",
    from: :my_app,
    gzip: false,
    only:
      ~w(css fonts images documents js favicon.ico robots.txt android-chrome-192x192.png android-chrome-512x512.png android-chrome-96x96.png apple-touch-icon.png favicon.ico favicon-16x16.png favicon-32x32.png mstile-150x150.png )

  plug Plug.Static,
    at: "/uploads",
    from: Path.expand("./uploads"),
    gzip: false

  # Code reloading can be explicitly enabled under the
  # :code_reloader configuration of your endpoint.
  if code_reloading? do
    socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
    plug Phoenix.LiveReloader
    plug Phoenix.CodeReloader
  end

  plug Plug.RequestId
  plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]

  plug Plug.Parsers,
    parsers: [:urlencoded, :multipart, :json],
    pass: ["*/*"],

    json_decoder: Phoenix.json_library(),
    length: 100_000_000

  plug Plug.MethodOverride
  plug Plug.Head
  plug Plug.Session, @session_options
  plug Pow.Plug.Session, otp_app: :my_app
  plug MyWebApp.Plugs.ReloadUserPlug
  plug MyWebApp.Router
end

Footnote.


I also tried creating a branch where I overwrote most of the :view to :html, then overwrote all the render/2 functions. I ran into a number of other errors though. For example, it was rendering something according to the new Layouts module and querying LayoutView somewhere, so I had both. Some forms required the new HTML module, but some still queried the old View module and I didn’t know why. I could go on. So I went back to the original tutorial where they kept the View where I’m now struggling with this.

Yeah, any older or sizable app can be a big challenge to upgrade and yes, will require numerous changes. Honestly, it’s not a task I would usually expect a lone junior to succeed with :grimacing:

I’m also seeing this error in an upgrade to 1.7 and Phoenix Component, but it’s only happening during tests.

I replaced my ErrorView with an ErrorHTML, identical to the one in created by mix phx.new, except that I uncommented the line:

  embed_templates "error_html/*"

I have an identical App.DataCase to the generated one and an App.ConnCase that is only different in that it imports Route helpers as well as verified routes. Here is my error html test:

defmodule AppWeb.ErrorHTMLTest do
  use AppWeb.ConnCase, async: true

  # Bring render_to_string/4 for testing custom views
  import Phoenix.Template

  test "renders 404.html" do
    assert render_to_string(AppWeb.ErrorHTML, "404", "html", []) =~ "Not found"
  end

  test "renders 500.html" do
    assert render_to_string(AppWeb.ErrorHTML, "500", "html", []) =~ "Error"
  end
end

Running the tests results in this error:

1) test renders 404.html (AppWeb.ErrorHTMLTest)
test/app_web/controllers/error_html_test.exs:7
** (KeyError) key :conn not found in: %{}
code: assert render_to_string(AppWeb.ErrorHTML, "404", "html", []) =~ "Oops!"
stacktrace:
  (app 0.1.0) lib/app_web/controllers/error_html/404.html.heex:8: anonymous fn/2 in AppWeb.ErrorHTML."404"/1

as well as a very similar one for the 500 test. The previous test were working when the same templates were traditional views.

Edit: In my case the issue was due to the templates actually expecting a @conn (to pass to Routes.static_url/2), so it’s not that relevant to this question.

But in case anyone else finds this thread via search and is encountering the same issue, the fix in this scenario was to just replace each @conn in the error page templates with AppWeb.Endpoint. This may or may not work for your app depending on what you were doing with the @conn.

1 Like

@John_Shelby Have you ever found a solution t this issue?
Currently I am at a similar point that my phx1.7 live_render(@conn, MyModule.Foo) does yield the same error.
After inspecting @conn down the whole stack its seems correct but after live_render/3 it gets overwritten into the %{__changed__: % ... } struct.

best

The idea is that you should not use @conn in rendering at all. One of the previously valid usage is for the route helpers, now you should use verified routes.

conn is something you should only access in the controller layer. In older version of Phoenix, conn was available as an assign, which was a violation of the controller/view layer split. The view layer should be pure functions on the assigns, and if you have @conn there, you could do nasty side-effects that are hard to debug.

I have left the project at version 1.6.9 for now. I was pressed for time and found this to be enough for the project for now. It works beautifully on the new project, but the transition was quite problematic in my case.

I still see live_render/3 available in LiveView.Component and it’s not deprecated (as of early June): Phoenix.Component — Phoenix LiveView v0.19.0