Serving Phoenix LiveView application on a subdirectory

We have a Phoenix application that makes extensive use of LiveView. A little background about the deployment situation:

  • It’s installed locally on the customer’s server, and they access it at a subdirectory of their machine, through a custom IIS site (this is all on Windows)

  • For example, the app itself might run on http://localhost:4200, and the user would access it at http://myserver.local/subdirectory/real-application

  • We use the IIS URL rewrite module to get everything after the subdirectory and give it to the “real” server, so in theory the Phoenix app shouldn’t have to worry about the subdirectory.

  • We don’t know the subdirectory at compile time.

The problem comes when connecting to the LiveView server. On mount the LiveView JavaScript module uses window.location.href to get the current URI from the browser, and then sends it to the server. From there, it’s decoded using the URI module and passed to Phoenix.Router.route_info/3. This function tries to match the URL to a route defined in router.ex. However, since the URL being passed is fetched using the browser API (in this example, it would be http://myserver.local/subdirectory/real-application), the call fails since it’s obviously not a real route.

The exact error we get is:

cannot invoke handle_params nor live_redirect/live_link to <url> because it isn't defined in <router>.

This is thrown from Phoenix.LiveView.Utils.live_link_info!/3 , which itself is called from Phoenix.LiveView.Channel.verified_mount/4. We’ve tried modifiying the LiveView source code, allowing a different URL to be passed to the server on mount. This stops the error above, but the connection still isn’t properly made.

We’re a little lost on how to solve this, so it would be great if someone could give us some tips :smiley:

i had a similar problem when trying to include a live-view as an embeddable widget on an any page of an arbitrary external CMS.

i solved it like this in app.js:

class MyLiveSocket extends LiveSocket {

  getHref() {
    let href=super.getHref(); 
    // ... manipulate href to your liking
    return href;
  }

}

...

let liveSocket = new MyLiveSocket("/live", Socket, {

4 Likes

Hello! I also had similar troubles with IIS and this is how I solved it, hope it is useful for posterity because working with subdirectories can get tricky on different levels.

Rewrite rule on IIS

After the IIS subdirectory project was setted up I just had to modify the routes via the url rewrite module, a simple rewrite like this would serve the page nicely:

<rewrite>
    <rules>
        <rule name="Rewrite" patternSyntax="ECMAScript">
            <match url="^subdirectory/(.*)" />
            <action type="Rewrite" url="http://localhost:4200/{R:1}" />
        </rule>
    </rules>
</rewrite>

But as @deerob4 mentions, the problem is when the browser requests resources directly from sources that will not match from the above rule:

The above ruleset will not match on any /subdirectory and will make request to resources that don’t exist; in my root.html.eex, Live Views and Live Components I’m using only static paths:

# root.html.eex
<script defer type="text/javascript" src="<%= Routes.static_path(@conn, ~s(/js/app.js)) %>"></script>

# liveview or livecomponent
<img src="<%= ~s(/images/logo.svg) %>">

There are many ways to solve this problem:

  • More rewrite rules with more specific routes
  • Just hardcode the paths or entire urls

But the most “practical” for me was to take advantage of the static path configuration for the web endpoint and plug.

Prepending your addresses with a subdirectory

On your config.exs or prod.exs configuration files:

# This will let helper functions like Routes.static_path know there's
# an implicit prepended path called "/subdirectory"
config :northbound, Web.Endpoint,
  static_url: [path: "/subdirectory"]

And on your endpoint.ex module:

# This will take care of actually serving all assets from the 
# "/subdirectory" path instead of root. Matching with our rewrites.
  plug Plug.Static,
    at: "/subdirectory",
    from: :app

So now our static pages helpers implicitly add the directory:

iex> Routes.static_path(@conn,  Routes.static_path(@conn, ~s(/js/app.js)) 
"/subdirectory/js/app.js"

But these functions can be hard to use on our liveviews since there’s no @conn object readily available, we could reference assets in our liveviews via the live helpers, but these are dependant on the router.ex configuration:

# So if your web endpoint is named Web.Endpoint...
# and your MainPage liveview is routed to "/main/live/page" 
iex> Routes.live_url(Web.Endpoint, Web.Liveview) 
"/main/live/page"

# Which will not match our rule

There are many ways to solve this:

  • Change your router and scope liveviews under /subdirectory
  • Build an intermediary function
  • Just hardcode the paths or entire urls

In the end I decided to build a “general” function that worked both on static and live pages:

def static_path(path \\ "") do
  static_path = Phoenix.Endpoint.Supervisor.static_path(Web.Endpoint) |> elem(1)
  Path.join(["/", static_path, path])
end

Keep in mind I only created this function because I needed to reference assets preferably with a static path from within my liveviews; I could totally be missing a more fitting solution. It is recommendable to test out the desired navigation before committing to any particular solution, because one size doesn’t seem to fit all here.

Connecting to the liveview

Here I prefered to take advantage of the url rewrite IIS module and add:

<rewrite>
  <rules>
    <rule name="WebSocket" patternSyntax="ECMAScript">
        <match url="^live/(.*)" />
        <action type="Rewrite" url="http://localhost:4200/live/{R:1}" />
    </rule>
    <rule name="Rewrite" patternSyntax="ECMAScript">
        <match url="^subdirectory/(.*)" />
        <action type="Rewrite" url="http://localhost:4200/subdirectory/{R:1}" />
    </rule>
  <rules>
</rewrite>

If hosting multiple liveview applications the above rule could “clash” with other subdirectories, I guess in that case you could change the /live endpoint to something like /app-live.


Sorry for overextending in probably obvious stuff, but it took me a while to get there and I’m hoping this info is useful. Many thanks to @geo’s, his blog post really helped me: Hosting A Phoenix App In A Subdirectory With Nginx.

2 Likes

I’m glad to know that that old article still is helpful. Nice job extrapolating that out to get the answer here!

1 Like

Saved me many hours :slight_smile: and guided me to a better solution in the end. Couldn’t do anything about the git history though :sweat_smile: (mine looks the same).