Django vs. ember-cli (and --proxy)
Our new application at work is an Ember frontend backed by Django. To facilitate communication between Django and Ember, we’re using rest_framework_ember, which coerces the default JSON format of Django REST Framework into the JSON format expected by Ember. Early in development, I tried to serve my Ember app while proxying API calls to Django, using:
ember serve --proxy http://localhost:8000
and was surprised to get an error message (in Chrome) that looked like this:
XMLHttpRequest cannot load http://localhost:8000/api/users/. No 'Access-Control-Allow-Origin'
header is present on the requested resource. Origin 'http://localhost:4200'
is therefore not allowed access.
The problem turns out to have to do with trailing slashes, but is a little more subtle than it appears.
Django, by default, expects resources to use a trailing slash, while Ember Data expects them not to. By itself, this isn’t a terrible problem: Django will send a 301 (PERMANENTLY REDIRECTED) code with a response to any request lacking a trailing slash, pointing it in the direction of the real resource. Ember, being a good netizen, reacts to the 301 as you’d expect – it makes a new request to the resource indicated by the 301.
Which is great, except if you’re running with –proxy.
When Django 301s a request, it responds with a fully qualified URL that the
client should direct further requests to. So a request for /api/users
will
be redirected to http://localhost:8000/api/users/
. ember-cli’s API proxy mode
is not quite smart enough to modify the 301 to point at itself. So from the
client’s perspective, it makes a request to http://localhost:4200/api/users
and is told to go look at http://localhost:8000/api/users/
instead. At which
point, cross-site-scripting attack prevention does its job, and shuts the whole
thing down.
So, what can we do about it? That depends on your Django project. The simple
solution is to set APPEND_TRAILING_SLASH = False
in settings.py
, which
will disable the redirect behavior. This only works, however, if your URL patterns
will still match without the slash at the end. In other words, make sure you’re
using
url(r'^users$', ...)
and NOT
url(r'^users/$', ...)
The second pattern needs to end a slash, or it won’t match – which means that
requests with APPEND_TRAILING_SLASH = False
will just 404.
If you’re using Django REST Framework with a Router, you’ll need to tell DRF (which generates URLs via the Router) not to use trailing slashes, i.e.:
router = DefaultRouter(trailing_slashes=False)
Credit where it’s due: rest_framework_ember’s README does warn you that you’ll need to do this to get around Django’s 301 behavior, but it doesn’t mention that a 301 might actually break your Ember app in development. That hadn’t occurred to me, so, here’s an extra warning.