3

I have a Django backend, VueJS frontend combination, where I serve a REST API via Django and a single page application with VueJS and vue-router.

From this question I got the tip to use the following urls in my main urls.py:

urlpatterns = [
    re_path(r'^(?P<filename>(robots.txt)|(humans.txt))$', views.home_files, name='home-files'),
    path('api/', include('backend.urls', namespace='api')),
    path('auth/', include('auth.urls')),
    path('admin/', admin.site.urls),
    re_path(r'^.*$', views.vue), # VueJS frontend
]

So I want URLs to behave like this:

{baseDomain}/api/users/1/ -> go to backend.urls  
{baseDomain}/auth/login/ -> go to auth.urls  
{baseDomain}/admin/ -> go to admin page  
{baseDomain}/de/home -> vue-router takes over

Now these URLs work perfectly fine, however I would expect that {baseDomain}/api/users/1 (without trailing slash) would still go to backend.urls, however what happens is that I land on the Vue page.

Adding APPEND_SLASH = True in settings.py does not help either, since it only appends a slash if it didn't find a page to load. But since the regex for my frontend matches anything it always redirects to Vue.

My attempt was to fix it by adding:

re_path(r'.*(?<!/)$', views.redirect_with_slash)

with the following code:

def redirect_with_slash(request):
    '''Redirects a requested url with a slash at the end'''
    if request.path == '/':
        return render(request, 'frontend/index.html')
    return redirect(request.path + '/')

But it isn't a very elegant one. Also mind the if request.path == '/':. Weirdly enough, Django would match '/' with the regex r'.*(?<!/)$' and then redirect to '//', which is an invalid URL and show an error page, so I had to include this if-statement.

Does anyone have a solution for this? In the referenced question this did not seem to be an issue, so I wonder why it is in my project.

EDIT: backend urls.py

"""
backend urls.py
"""
from django.urls import include, path
from rest_framework_nested import routers
from auth.views import UserViewSet, GroupViewSet, ProjectViewSet
from .views import IfcViewSet, IfcFileViewSet

app_name = 'api'

router = routers.DefaultRouter() #pylint: disable=C0103
router.register(r'users', UserViewSet)
router.register(r'groups', GroupViewSet)
router.register(r'projects', ProjectViewSet)
projects_router = routers.NestedSimpleRouter(router, r'projects', lookup='project')
projects_router.register(r'models', IfcFileViewSet, base_name='projects-models')

urlpatterns = [
    path('', include(router.urls)),
    path('', include(projects_router.urls))
]

"""
auth urls.py
"""
from django.urls import path, include
from rest_framework import routers
from rest_framework_jwt.views import obtain_jwt_token, refresh_jwt_token
from .views import RegistrationViewSet

app_name = 'authentication'
router = routers.DefaultRouter()
router.register('register', RegistrationViewSet)

urlpatterns = [
    path('', include(router.urls)),
    path('', include('rest_auth.urls')),
    path('refresh_token/', refresh_jwt_token),
]
Truning
  • 107
  • 1
  • 10
  • Please add your `backend.urls`. – heemayl Nov 21 '19 at 09:28
  • @heemayl Added them just now. I use https://pypi.org/project/django-rest-framework-nested/ do you think that could be the problem? – Truning Nov 21 '19 at 09:38
  • @heemayl but shouldn't the urls in auth urls.py work then? I import vanilla DRF here and still {baseDomain}/auth/login does not behave like {baseDomain}/auth/login/. – Truning Nov 21 '19 at 10:00

1 Answers1

0

The problem is that you have a catch-all in re_path(r'^.*$', views.vue), so if any URL is not matched exactly on the earlier paths, this will be triggered.

Django's CommonMiddleware actually appends a trailing slash and redirect, when it finds a 404 and the URL path does not end in / (depending on the APPEND_SLASH setting), but that's on response.

In you case, you can have a tiny request middleware that appends trailing slash if the request path not end in / e.g.:

from django.shortcuts import redirect

class AppendTrailingSlashOnRequestMiddleware:

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):

        if not request.path.endswith('/'):
            query_string = request.META['QUERY_STRING']
            query_string = f'?{query_string}' if query_string else ''
            to_url = f'{request.path}/{query_string}'
            return redirect(to_url, permanent=True)

        response = self.get_response(request)

        return response

Add the middleware to settings.MIDDLEWARE obviously, preferably put it at the top to prevent unnecessarily processing from other middlewares as we'll be redirecting anyways and processing would be required then as well.


But this has an issue; the data from POST/PUT/PATCH will be lost when doing redirection (here we're doing 301 but similarly applicable for 302. There's Temporary Redirect 307 that can help us in this regard and the good thing is all the regular browsers including IE support this. But Django does not have this out of the box; so we need to implement this ourselves:

from django.http.response import HttpResponseRedirectBase

class HttpTemporaryResponseRedirect(HttpResponseRedirectBase):
    status_code = 307

Now, import that in the middleware, and use it instead of redirect:

class AppendTrailingSlashOnRequestMiddleware:

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):

        if not request.path.endswith('/'):
            query_string = request.META['QUERY_STRING']
            query_string = f'?{query_string}' if query_string else ''
            to_url = f'{request.path}/{query_string}'
            return HttpTemporaryResponseRedirect(to_url)  # here

        response = self.get_response(request)

        return response

N.B: If you want to keep the browser caching facilities for GET, you can redirect to 301/307 based on request.method.

heemayl
  • 39,294
  • 7
  • 70
  • 76
  • Works like a charm! Thank you so much. Two follow-up questions though: What is the difference between temporary and permanent redirect (i.e. HTTP 308)? Should I do an if statement where I give status_code 301 for GET and HEAD and 308 for POST etc.? And second question: Why can't I find this anywhere else? Is my setup that out of the ordinary? Is there a smarter way to serve a single page webapp with Django? Thanks again – Truning Nov 22 '19 at 13:20
  • @Truning Wow. So many questions :) Browser will cache all permanent redirects whereas the temporary ones aren't cached. The way I serve my SPA with DRF is that Nginx sits in front and sends all requests to Vue frontend (which has it's own routing enabled to access various stuffs) except the backend API routes, which goes straight to the uWSGI (and in turn Django/DRF). – heemayl Nov 22 '19 at 16:35
  • Yes many questions, sorry for that :D Perfect, thank you for your answers. I guess something similar can be achieved with Apache too? – Truning Nov 22 '19 at 16:51