Subscribe to access all episodes. View plans →

#151: Redirect Requests with PlugCanonicalHost

Published August 31, 2022

Phoenix 1.6

plug_canonical_host 2.0

Follow along with the episode starter on GitHub


Here we have our album application and while it works great there is a problem. Currently, we’re accessing it from new.teacher:4000, however if we open our other browser tab we see that it can also be accessed from old.teacher:4000.

In a production application you may have this issue if you’ve changed domains or more commonly you have an application that is accessible at the naked domain and with the www subdomain. For our example, we want our canonical host to be new.teacher:4000 and we want anyone who visits old.teacher:4000 we want to be redirected with an HTTP status of 301.

Now there are a few different ways we could handle this, but for our example let’s do this at the application level using the PlugCanonicalHost package. Let’s get started. We’ll go to Hex and grab the plug_canonical_host package and then we’ll add it to our Mixfile.

mix.exs

...
defp deps do
...
  {:plug_canonical_host, "~> 2.0"},
...
end
...

Once it’s added we can go to the command line and download it with mix deps.get:

$ mix deps.get
...
New:
  plug_canonical_host 2.0.1
* Getting plug_canonical_host (Hex package)

With that we can open our endpoint.ex and we’ll add a plug named plug :canonical_host to the top of the module so we can handle the redirect before any other plugs are called here. Then let’s define a new private function plug named canonical_host if you’re new to plugs check out episodes 19 - function plugs and 20 - module plugs.

We’ll need it to accept the conn and opts , which we can ignore since we wont need them for this example. This function will be pretty simple. In it we’ll get the canonical host and then call PlugCanonicalHost to handle the redirect.

It’s recommended to define your canonical host with an environment variable, so let’s open up our dev.exs and add a config for our host. For a production application we would load the canonical host from the environment variable, but for simplicity let’s add our host here, new.teacher.

config/dev.exs

...
config :teacher,
  canonical_host: System.get_env("CANONICAL_HOST")
...

With that added let’s go back to the endpoint.ex module and grab our canonical_host. Then if it exists we’ll call PlugCanonicalHost.init including the canonical_host to get the options which we can then use to call PlugCanonicalHost.call passing in the connection and the options.

And if we don’t have a canonical_host we’ll let the request proceed and return the conn.

lib/teacher_web/endpoint.ex

...

plug :canonical_host

defp canonical_host(conn, _opts) do
  canonical_host = Application.get_env(:teacher, :canonical_host)
  if canonical_host do
    opts = PlugCanonicalHost.init(canonical_host: canonical_host)
    PlugCanonicalHost.call(conn, opts)
  else
    conn
  end
end

...


Let’s test our changes. We’ll go to the command line and start the server.

$ mix phx.server
...

Alright, from old.teacher:4000 let’s refresh the page. When we do we are redirected to new.teacher:4000. This looks great, but let’s do one more test.

Going back to the command line we’ll run curl with the “silent” and “head” option requesting old.teacher and great - when we do we can see the HTTP status of 301 was returned.

$ curl -sI "http://old.teacher:4000"
HTTP/1.1 301 Moved Permanently
cache-control: max-age=0, private, must-revalidate
content-length: 241
date: 
location: http://new.teacher:4000/
server: Cowboy

Our redirect is working and it’s being applied to all pages in our application, but what if we have certain pages we don’t want to redirect. How could we exclude them? For our example, let’s say that we want “/blog” and all the blog posts to be excluded from the canonical host redirect.

To do that we’ll go back to our endpoint.ex module. And we’ll add two more canonical_host functions. The first one to pattern match on the individual blog post returning the conn to allow the request to proceed. Then another to pattern match on the blog index page.

lib/teacher_web/endpoint.ex

...

defp canonical_host(%Plug.Conn{request_path: "/blog/" <> _id} = conn, _opts) do
  conn
end
defp canonical_host(%Plug.Conn{request_path: "/blog"} = conn, _opts) do
  conn
end

...

With those changes let’s open “/blog” in our browser and we can see it’s old.teacher:4000/blog. Let’s try to refresh the page and see if it redirects.

Great - when we do it’s not redirect. Our blog is being excluded. We can confirm by going to the command line and running the curl command for our blog routes…and when we do we can see that neither is being redirect. PlugCanonicalHost is a great if you need a flexible library that allows you to quickly set up redirects based on your hostname.

© 2024 HEXMONSTER LLC