Disable Live Reload on certain routes or pipelines?

Is it possible to disable Live Reload for certain routes?

A section of my Phoenix application serves files on a particular route by streaming them in chunks from an Elixir Stream sourced from a MongoDB document collection. Users upload zip files containing a small web application (HTML, Javascript, CSS, Images, etc…), and these are unzipped and stored in MongoDB. This works great in release mode AND in dev mode for non-html files. But in dev mode, if the content-type is “text/html”, Live Reload attempts to inject code into it and I get an error:

** (exit) an exception was raised:
    ** (ArgumentError) argument error
        :erlang.iolist_to_binary(nil)
        (phoenix_live_reload) lib/phoenix_live_reload/live_reloader.ex:95: anonymous fn/2 in Phoenix.LiveReloader.before_send_inject_reloader/2
        (elixir) lib/enum.ex:1811: Enum."-reduce/3-lists^foldl/2-0-"/3
        (plug) lib/plug/conn.ex:1133: Plug.Conn.run_before_send/2
        (plug) lib/plug/conn.ex:452: Plug.Conn.send_chunked/2

The stack trace in the browser shows the error to occur only in HTML files, and fails in the call to IO.iodata_to_binary/1

defp before_send_inject_reloader(conn, endpoint) do
  register_before_send(conn, fn conn ->
    if 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 <> reload_assets_tag(conn) <> Enum.join(["</body>" | rest], "")
        put_in conn.resp_body, body

Here’s the code I’m using to send the files. The call to TagRepo.Stream.stream/3 returns a tuple containing an Elixir Stream created with Stream.resource/3 and the content-type of the file.

defmodule ApiServicesWeb.Download do
import Plug.Conn

  def init(opts), do: opts

  def call(conn, _opts) do
    case TagRepo.Stream.stream(conn.params["username"], conn.params["webapp_name"], Enum.join(conn.params["path"], "/")) do
      :not_found -> 
        conn = conn
        |> send_resp(404, "file not found")
        |> halt()
      {stream, content_type} -> 
        conn = conn
        |> put_resp_content_type(content_type)
        |> send_chunked(200)
      
        Enum.into(stream, conn)
    end
  end
end

My immediate thought was to turn off the Live Reload feature completely for this application. But, of course, it’s really handy for development mode, and I want it to work. Is there a way to prevent it from injecting the live reload code for HTML files? Or only for files on a certain route? Or a certain pipeline?

Live reloading is applied through these lines in endpoint.ex:

# 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

Since they are in endpoint.ex they are applied globally, but you can move them anywhere you want. You will need to grab the code_reloading? boolean yourself from the application config. So it’s possible, though I’m not sure what the nicest way to accomplish it is. Ideally you could conditionally unregister plugs so they can still be applied globally and disabled on a per-needed basis, though I’m not sure if that’s possible.

What you can do though is leave the socket definition in endpoint.ex and move the two plugs into their own pipeline in your router, then apply that pipeline to just routes/scopes you need. For example, in router.ex you can create:

pipeline :live_reload do
  plug Phoenix.LiveReloader
  plug Phoenix.CodeReloader
end

And then within some scope that you want live reload on you can do:

@live_reload? Application.get_env(:my_app, MyAppWeb.Endpoint)[:code_reloader]

scope "/", MyAppWeb do

  if @live_reload? do
    pipe_through [:live_reload]
  end

  pipe_through [:browser, :frontend, :whatever_else]

  get "/foo" ...

end

It’s kind of annoying to have to create a new plug pipeline and add it to all of your routes rather than being able to disable it on a single route. There might be a way to do the latter though that I’m unaware of. This is just my take on it.

1 Like

I thought of an alternative that might allow you to keep a globally applied plug that will skip certain routes. I didn’t test this, so I’m not 100% sure it will work, but I took inspiration from the way the Phoenix.LiveReloader plug responds to the live reload iframe request versus a regular page request.

Basically, match on conn.path_info inside of the plug and if it matches some route then just return the unmodified conn, but if it doesn’t match then pass the conn through the LiveReloader and CodeReloader plug that are typically in endpoint.ex

defmodule ConditionalLiveReloadPlug do
  import Plug.Conn

  @live_reload Application.get_env(:my_app, MyAppWeb.Endpoint)[:code_reloader]

  def init(_), do: nil

  # If path begins with /some/path/prefix/ then don't pass conn through the
  # live reload plugs
  def call(%Plug.Conn{path_info: ["some", "path", "prefix" | _]} = conn , _opts) do
    conn
  end

  def call(conn, _opts) do
    if @live_reload do
      conn
      |> Phoenix.LiveReloader.call([])
      |> Phoenix.CodeReloader.call([])
    else
      conn
    end
  end
end

If this works (questionable) then you should be able to replace the Phoenix.LiveReloader and Phoenix.CodeReloader plugs in endpoint.ex with just this plug and not have to change any pipelines in your router.

1 Like

Which phoenix_live_reload version are you using? The nil error is a bug that has been fixed in version 1.1.1.

1 Like

I’ve currently got phoenix_live_reload 1.1.0. I was thinking that the nil error in this case might be because I’m using a Stream to feed the response instead of a standard built response body.

That should not affect it, if it does, it is a bug. Please try 1.1.1. :slight_smile:

Switching to phoenix_live_reload 1.1.1 solved the problem. Many thanks!

@kylethebaker … many thanks to you as well for your detailed suggestions. I’ve spent the last few hours reading source code, spurred on by your suggestions. Learned a lot.

you can also use exclusion

~r{^lib/www_domain_com_web/templates/front/.*(eex|md)$}

and I guess fancy Regex