Django Behind a Proxy
With the rise in number of microservices being used at WorkIndia, the old system of using a separate subdomain for each service along with a dedicated Elastic Load Balancer was quickly proving to be too expensive. Thus, a decision was taken to move all microservices under single subdomain using a single ELB. We also added a layer of Cloudfront to decrease costs.
But soon, we ran into some problems…
Incorrect Host Headers
The addition of Cloudfront proxy meant that some Django applications won’t get the correct Host
headers. This generally isn’t a big deal, but one of our services used this extensively. (Pagination using Django Rest Framework uses it to create next and previous url)
For eg, Without a proxy, the host header for a sample request like https://api.workindia.in/api/health/
looked like this: Host: api.workindia.in
But with a proxy, the header changed to Host: api.workindia.in:20000
, because that was the port on which our ELB was configured.
Solution
Luckily, the developers of Django foresaw this and created a setting that could override the Host
header with a custom header X-Forwarded-Host
.
USE_X_FORWARDED_HOST = True
Adding the above line in your settings file will make the application first check the X-Forwarded-Host
. If the request contains X-Forwarded-Host
header, it will be used as the value for current host.
X-Forwarded-Host Header Not Present
Feeling confident, I deployed the service in production. But to my disappointment Django was still using the value being passed in the Host
header.
The above solution wasn’t enough, because neither Cloudfront nor the Load Balancer set the X-Forwarded-Host
header automatically. Thus the request didn’t contain the required header.
Solution
Cloudfront allows you to add custom headers to an origin which will be attached to all requests to that origin. Following this guide, we added the following header X-Forwarded-Host: api.workindia.in
.
Incorrect Protocol — HTTP instead of HTTPS
Now that our request had the X-Forwarded-Host
header, we were able to get the port out of our host. Yet, after deployment we couldn’t get it to work.
I realised that although the host was correct in the next url, the protocol wasn’t. Instead of https
, the url was using http
.
Solution
Again Django settings to the rescue. The SECURE_PROXY_SSL_HEADER
setting can be used to specify which header and value determines if the request is https
or http
.
It takes a tuple with exactly 2 element:(Header-Name, Header-Value)
For eg. SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
(Remember, Django automatically adds 'HTTP_'
to the start of x-header names before making the header available in request.META
.)
This tells Django to trust the request is secure (i.e.https) if the header X-Forwarded-Proto
has a value of https
.
Although, this didn’t work in my case because the ELB would always override X-Forwarded-Proto
to http
.
So, I added a custom header to my Cloudfront Origin (like we did above) called X-Custom-Proto: https
I also changed the settings file as such:SECURE_PROXY_SSL_HEADER = ('HTTP_X_CUSTOM_PROTO', 'https')
Et voilà! Everything started working perfectly.