31
Since this is the first SO post that comes up in google search result, I think it would be useful to share how I solved it. Mine has a Kubernetes flavor, but under the hood the logic is not so different than the others.
I’m using Kubernetes ingress controller sitting in front of Django, so things might be different to you, but I’ll summarize my context so you can see if it’s useful to you:
The Problem
So I got the same issue as OP, the pagination link from DRF is always http
even if I’m hitting the api endpoint using https
. You can see this more clear if you have setup the browser API which comes with the Django REST Framework (DRF), and go to the api root page. I see all links that’s generated by DRF are all http
regardless of what protocol I use when visiting the site.
My Context
The stack, by order from external world till Django:
- Kuberenetes Nginx Ingress Controller (w/ SSL setup using letsencrypt)
- Ingress rule points to Django service
- Gunicorn starts the Django server
- Django reads the
settings.py
and start serving requests
How I Debug and Found out the Cause
- Dump
nginx.conf
of the ingress controller. In case you don’t know how to to this: first figure out the pod name bykuberctl get pods -n <namespace of your ingress controller>
, write that name down, then dump the file bykubectl exec -it -n ingress_controller_namespace ingress_controller_pod_name cat /etc/nginx/nginx.conf > nginx.conf
. - Look at the
nginx.conf
, check if you haveproxy_set_header X-Forwarded-Proto $scheme;
in thelocation
section for your website domain, or similar thing that sets the proxy headerX-Forwarded-Proto
properly. By default, the Kubernetes nginx ingress controller should already have this line (or the equivalent) for you. - Next the request is directed to Gunicorn. One gotcha is you will want to add
--forwarded-allow-ips="*"
to your gunicorn command, or if you know your nginx server IP, you can limit down to that one, so that gunicorn will forward the headers for you, otherwise gunicorn will strip off those headers. So you end up with command likegunicorn django_server.wsgi:application --forwarded-allow-ips="*" --workers=${PROPER_WORKER_NUM} --log-level info --bind 0.0.0.0:8001
. You can have 4 workers by specifyingworkers=4
, usually depends on the CPU you have on the server. The--log-level info
is just for debug purpose, it’s optional. - In Django, you will need to have
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
insettings.py
, which other answer has mentioned. You may wonder why the prefixHTTP_
while in nginx our header name isX-Forwarded-Proto
. This is because WSGI will add the prefix to headers that it recognizes. This line basically says, if the headerHTTP_X_FORWARDED_PROTO
equals to the string"https"
, then Django recognizes the request as secure. This will affect several behaviors in Django, e.g. you will getrequest.is_secure == True
,request.build_absolute_uri(None) == 'https://...'
, and most important, Django REST Framework pagination link will now usehttps
! (As long as you indeed hit the api byhttps
)
Alright, now you can test again. If DRF gives you https now – congrats. Or, if you are same as me, after trying above, still no luck – DRF still generating the damn http
links. I want to share some tipbits when I was debugging like hell:
Print out the following values in Django, either by print()
, if you have DEBUG=True
and have access to the server log, or just pass them to template context and show them in a html page, if that makes it easier for you to test in a production environment where SSL is enabled.
request.is_secure()
: if you’re failing to get DRF to usehttp
, chances are you are gettingFalse
.request.META
gives you all headers that Django received. Did the headerHTTP_X_FORWARDED_PROTO
show up? What value is it?- I want to share that this is the a-ha moment for me: the value of
HTTP_X_FORWARDED_PROTO
ishttps,https
, which tells me that it somehow having duplicated values, and I probably haveproxy_set_header
set twice, and Bingo! By default K8 ingress controller already has this line, and since I added it again through alocation-snippet
, now I have two lines ofproxy_set_header X-Forwarded-Proto $scheme;
in total. After removing my snippet, DRF showshttps
and I was so so happy to get this done. It wasn’t that straight forward because I thoughtproxy_set_header
will overwrite and “set” the header value. But seems like it just keep appending.
- I want to share that this is the a-ha moment for me: the value of
10
Do I need to set that Django SECURE_PROXY_SSL_HEADER? I wasn’t sure, given the warning that it’s potentially insecure.
Yes you do.
However, you need to take care about what’s your doing. In particular making sure it drop the X-Forwarded-Proto from outside.
- [Django]-Homepage login form Django
- [Django]-Celery unable to use redis
- [Django]-How to update multiple fields of a django model instance?
0
This answer may not be the best one, but it works and there is no need to change nginx and django SECURE_PROXY_SSL_HEADER.
in settings.py:
MY_PROTOCOL = "https"
in views.py:
from django.conf import settings
class MyView(generics.ListAPIView):
serializer_class = MySerializer
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, *args, **kwargs):
response = super().get(self, request, *args, **kwargs)
if settings.MY_PROTOCOL == "https":
if response.data["next"]:
response.data["next"] = response.data["next"].replace(
"http://", "https://"
)
if response.data["previous"]:
response.data["previous"] = response.data["previous"].replace(
"http://", "https://"
)
return response
- [Django]-Django Setup Default Logging
- [Django]-Django queryset filter – Q() | VS __in
- [Django]-Get path of virtual environment in pipenv