2

I was thinking of skipping Phoenix since I am just planning to build a demo for a React app that uses some API routes for its state. Seems like an opportunity to familiarize with the underlying tech.

I came up with the following, but it feels very "hardcoded" and I am curious if there is a more elegant solution to achieve the same thing.

defmodule DemoApp.Plug.ServeStatic do
  use Plug.Builder

  @static_opts [at: "/", from: "priv/static"]

  plug :default_index
  plug Plug.Static, @static_opts
  plug :default_404
  plug Plug.Static, Keyword.put(@static_opts, :only, ["error_404.html"])

  # Rewrite / to "index.html" so Plug.Static finds a match
  def default_index(%{request_path: "/"} = conn, _opts) do
    %{conn | :path_info => ["index.html"]}
  end
  def default_index(conn, _), do: conn

  # Rewrite everything that wasn't found to an existing error file
  def default_404(conn, _opts) do
    %{conn | :path_info => ["error_404.html"]}
  end
end

The idea is to have / serve index.html without redirect, and serve the contents of an error file whenever something isn't found, instead of a minimal response "404 file not found" string.

Is there a way to achieve this without plugging Plug.Static twice, or is this the way to go? I can also see the catchall :default_404 collide with my API routes later on, and I am not sure how I would resolve that.

Any input would be much appreciated. Thank you!

MildlySerious
  • 8,750
  • 3
  • 28
  • 30

1 Answers1

7

I would use Plug.Router and Plug.Conn.send_file/5 for this. Here's some code that does what you do but much cleaner:

defmodule M do
  use Plug.Router

  plug Plug.Static, at: "/", from: "priv/static"
  plug :match
  plug :dispatch

  get "/" do
    send_file(conn, 200, "priv/static/index.html")
  end

  match _ do
    send_file(conn, 404, "priv/static/404.html")
  end
end

As the :match and :dispatch are plugged in after Plug.Static, any files in priv/static will be served before falling back to the router, just like Phoenix does.

With these files in priv/static:

➜ cat priv/static/404.html
404.html
➜ cat priv/static/index.html
index.html
➜ cat priv/static/other.html
other.html

Here's how this code works:

➜ curl http://localhost:4000
index.html
➜ curl http://localhost:4000/
index.html
➜ curl http://localhost:4000/index.html
index.html
➜ curl http://localhost:4000/other.html
other.html
➜ curl http://localhost:4000/foo
404.html
➜ curl http://localhost:4000/foo/bar
404.html
➜ curl http://localhost:4000/404.html
404.html
➜ curl -s -I http://localhost:4000/foo | grep HTTP
HTTP/1.1 404 Not Found
➜ curl -s -I http://localhost:4000/foo/bar | grep HTTP
HTTP/1.1 404 Not Found
➜ curl -s -I http://localhost:4000/404.html | grep HTTP
HTTP/1.1 200 OK
Dogbert
  • 212,659
  • 41
  • 396
  • 397
  • Thank you, that's exactly what I was looking for! I checked the `Plug.Router` docs and I was using the built pipeline in the OP in one, but completely forgot that I had all of the `Plug.Conn` methods at my disposal as well. Much appreciated. – MildlySerious Jul 08 '16 at 16:03
  • 2
    One issue with this approach is that `send_file` doesn't have all of the features of `Plug.Static`, such as setting caching headers like `ETag`. Is there a way to just rewrite the conn's path and forward the modified request to `Plug.Static`? – Denis Washington Feb 09 '18 at 06:50
  • You can also combine Plug.Router and Plug.Static to enable file caching: https://stackoverflow.com/a/51155884/285691 – Nikolai Koudelia Jul 03 '18 at 13:39