2
votes

I'm working on an API server implemented with the Phoenix Framework. JSON APIs are served from the /api/<version> URL. The server also needs to serve static .html (and others, .css, .js, etc.) files from / : it's actually a little SPA developed in another language and compiled down to HTML5, JS and CSS. The behavior should be:

  • / should serve the index.html file;
  • /some/file.html should serve the file.html file from the some directory provided the path exists and 404 if it doesn't;
  • URLs like /some/ should send back a 403 or a 404 to prevent directory listing
  • everything under /api is managed by dedicated controllers.

The project is generated with mix phx.new --no-ecto --no-html --no-gettext --no-webpack static to get rid of .css, .js files and of the templates. I also created a priv/static directory for the statics assets.

I had to remove only: ~w(css fonts images js favicon.ico robots.txt) from the parameter list of plug Plug.Static in endpoint.ex to have files at the root URL being served. All works fine except the errors:

  • a request on / displays the Phoenix error page with the log [debug] ** (Phoenix.Router.NoRouteError) no route found for GET / (StaticWeb.Router). I added {:plug_static_index_html, "~> 1.0"} so I got rid of that problem, yet invoking /subdir sends back Phoenix error page. I just don't see where and how to tell Phoenix to send back a 403 or a 404 (except for /).
  • Invoking a URL on a non-exiting file does not send back a 404 but rather the Phoenix error page. I tried to create a static pipeline in router.ex but it seems the flow doesn't get there. The documentation states: If a static asset cannot be found, Plug.Static simply forwards the connection to the rest of the pipeline. but I don't see where to put the 404 reply.

Here are the two configurations I tried:

Conf I

# endpoint.ex
...
  # Serve at "/" the static files from "priv/static" directory.
  plug Plug.Static.IndexHtml, at: "/"
  plug Plug.Static,
    at: "/",
    from: :static,
    gzip: false
    # only: ~w(css fonts images js favicon.ico robots.txt)
...

Conf II

I removed every thing concerning static content from endpoint.ex and added a static pipeline in router.ex

# router.ex
...
  pipeline :static do
    plug :put_secure_browser_headers
    plug Plug.Static,
      at: "/",
      from: :static,
      gzip: false # ,
      # only: ~w(css fonts images js favicon.ico robots.txt)
    plug :not_found
  end
  
  scope "/", staticWeb do
    pipe_through :static
  end

  def not_found(conn, _) do
    send_resp(conn, 404, "not found")
  end
...

Any hint would be helpful.

Update on 13th of August 2020

Added a catch all rule on scope "/" at the end of router.ex and at least I get a 404 for any wrong request. I'm just wondering how clean all this is...

# router.ex
...
  scope "/", AlaaarmWeb do
    match :*, "/*path", DefController, :error_404
  end
1

1 Answers

2
votes

I had your same problem, I was trying to serve a SPA created using create-react-app, and I put all the generated files in the priv/static folder.

The only way I find to serve the static index.html file from the root path is the following:

  • I had to remove the only: option form the Static.Plug in the endpoint.ex file as you did.
  • Following this answer: https://stackoverflow.com/a/37568770/8620481, I adopted a different strategy. Instead of trying to serve the static file directly, I read its content as a string and then serve it from a controller.

In particular, in the router.ex file, under the "/" scope, I have the following route defined:

get "/", PageController, :index

In the PageController, I implemented the index method as follows:

def index(conn, _params) do
  file = File.read!("priv/static/index.html")
  html(conn, file)
end

I don't know exaclty what is the mechanic behind the Static Plug, but I found this workaround acceptable for my use case.

I hope this could help you as well.