CORS in Phoenix
Single page applications
Single page applications (SPA) are becoming more and more popular, often replacing the more traditional server rendered websites that are common in Rails or PHP. You have options such as Angular, Ember, React, and there are new ones popping up every day it seems. The issue with SPAs is that they can't live on their own... they need some sort of backend to get data from and to post data to. This API also handles authentication and permissions among other things. SPAs are basically useless without a backend.
Phoenix as an API
There are some special considerations you have to think of when creating an API that will be consumed by Angular (or any other front-end framework). One of these is security related, specifically around the issue of CORS.
What is CORS?
In other words, there needs to be an agreement in place that the browser making the request to the server is allowed to do so. This is done through a series of Request and Response Headers.
Modern browsers go a step further and make an additional request before the main request which is sometimes called a "pre-flight" request, which asks the server if the request it really wants to make will be permitted or not. Here is an example of the pre-flight request:
OPTIONS / Host: bar.com Origin: http://foo.com
And the response:
Access-Control-Allow-Origin: http://foo.com Access-Control-Allow-Methods: PUT, DELETE
Handling CORS in Phoenix
If you're used to Express in Node, or Rails in Ruby, you're most likely familiar with middleware... or maybe familiar with
before_action filters in Rails. Elixir has something similar which is called plug.
There are a couple of plugs you can use in Phoenix to intercept OPTIONS requests (and respond with the appropriate headers), and to append the CORS headers to all other requests. CORSPlug is one of these options along with Corsica. Both essentially do the same thing.
To use them, you add a plug to the
:api pipeline in your routes:
pipeline :api do plug CORSPlug, [origin: "http://localhost:9001"] plug :accepts, ["json"] end
One gotcha for pre-flight requests
In Phoenix, a plug is only ever called if there is a matching route. Because of this I ran into a problem where the OPTIONS (or pre-flight) requests were failing with a 404 response because the route didn't exist, and therefore the plug which is supposed to intercept the request wasn't run.
To solve this I had to define
options routes for my different endpoints so that the plug gets called correctly.
scope "/api", ElixirApp do pipe_through :api options "/auth/github", AuthController, :options post "/auth/github", AuthController, :github resources "/articles", ArticleController options "/articles", ArticleController, :options options "/articles/:id", ArticleController, :options end