phoenixframework / phoenix_live_reload

Provides live-reload functionality for Phoenix
MIT License
315 stars 90 forks source link

Live Reload isn't joining the channel #115

Closed krainboltgreene closed 6 months ago

krainboltgreene commented 3 years ago

I'm at wits end with this issue, because I have done a ton of digging. Here are the two facts that I know:

  1. When I'm looking at my HTML content there's no join message to phoenix:live_reload.
  2. When I'm looking at the dashboard there is.

I have confirmed that:

  1. The iframe is loading
  2. The code inside the iframe runs.
  3. Both websocket requests going to /phoenix/live/socket are exactly the same (except the key)
  4. There are no errors.

I am in a unique situation however, in that my application is running on Github Codespaces. I'm not accessing my application at localhost:4000, but rather a unique URL github assigns my application.

Some things that are different between the two page loads:

  1. Sometimes the iframe has no response data, but I think this is a chrome and firefox issue.
  2. When the iframe has no response data, the websocket doesn't get made.
  3. My websocket request is marked as "provisional" by chrome.
  4. When I write my own socket connection to the exact same channel and join the request isn't provisional.
krainboltgreene commented 3 years ago

Okay so I've done some deeper digging. I had a hypothesis: Perhaps the chan.join() isn't firing? The only way to prove that it was actually firing was to utilize a socket constraint in the phoenix.js library that stops you from double joining. After some hacking I duplicated the chan.join() line and there was an error!

So somewhere in my browser a channel is being joined for the purposes of the constraint, but not joining for the purposes of sending the message.

krainboltgreene commented 3 years ago

I found this interesting tidbit about websockets in iframes: https://stackoverflow.com/a/47900288

krainboltgreene commented 3 years ago

Okay, so I can't tell you why this works, but I've rewritten the plug to just shove the js directly into body, instead of via an iframe, and now it works 100%:

defmodule Reloader do
  import Plug.Conn
  @behaviour Plug

  def init(opts) do
    opts
  end

  def call(conn, _) do
    endpoint = conn.private.phoenix_endpoint
    config = endpoint.config(:live_reload)
    patterns = config[:patterns]

    if patterns && patterns != [] do
      before_send_inject_reloader(conn, endpoint, config)
    else
      conn
    end
  end

  defp before_send_inject_reloader(conn, endpoint, config) do
    register_before_send(conn, fn conn ->
      if conn.resp_body != nil and html?(conn) do
        resp_body = IO.iodata_to_binary(conn.resp_body)

        if has_body?(resp_body) and :code.is_loaded(endpoint) do
          [page | rest] = String.split(resp_body, "</body>")
          body = [page, inject_reload_sript(conn, endpoint, config), "</body>" | rest]
          put_in(conn.resp_body, body)
        else
          conn
        end
      else
        conn
      end
    end)
  end

  defp html?(conn) do
    case get_resp_header(conn, "content-type") do
      [] -> false
      [type | _] -> String.starts_with?(type, "text/html")
    end
  end

  defp has_body?(resp_body), do: String.contains?(resp_body, "<body")

  defp inject_reload_sript(conn, endpoint, config) do
    IO.iodata_to_binary(["""
    <script type="application/javascript">#{read_external_javascript(:phoenix, "priv/static/phoenix.js")}</script>
    <script type="application/javascript">
      var socket = new Phoenix.Socket("/phoenix/live_reload/socket");
      var interval = 100;
      var targetWindow = "top";
      #{read_external_javascript(:phoenix_live_reload, "priv/static/phoenix_live_reload.js")}
    </script>
    """])
  end

  defp read_external_javascript(application, file) do
    Application.app_dir(application, file)
      |> File.read!
      |> String.replace("//# sourceMappingURL=", "// ")
  end
end