Django-rest-framework: Option to have default router accept URLs without trailing slash

Created on 2 Jun 2013  Â·  22Comments  Â·  Source: encode/django-rest-framework

It'd just be nice to be able to create a default router which, when included in a URL configuration file, will accept URLs without a trailing slash.

This is entirely the fault of Angular.js which strips the slashes off its URLs and doesn't show any sign of changing. There are other developers who have had the same problem. The main issue is that Angular will POST without the slash and Django can't redirect that.

I know that it's possible to have this by building a router from scratch and using custom URLs for everything but it seems like a very bad way of doing things.

Ideally, I'd like to be able to initialize my router with something like

router = routers.DefaultRouter(trailing_slash='optional')
router.register(r'Place', PlaceViewSet)

which would accept URLs of the form Place/?$ instead of Place/$

Enhancement

Most helpful comment

Complete solution with Django 1.11

from django.conf.urls import include, url
from rest_framework.routers import DefaultRouter

from api.v1.views import BookViewSet

router = DefaultRouter()
slashless_router = DefaultRouter(trailing_slash=False)
slashless_router.registry = router.registry[:]
router.register(r'^books', BookViewSet, base_name='books')

urlpatterns = [
    url(r'^', include(router.urls)),
    url(r'^', include(slashless_router.urls)),
]

All 22 comments

Seems reasonable, yeah. How would you feel about a simple Boolean? I'm not sure if an 'optional' case is really necessary/desirable.

Boolean is totally fine.

I solved my own problem temporarily. I just extended the SimpleRouter class
and copy pasted the routes from the original then put question marks at the
end of all the routes.
On 02/06/2013 7:03 PM, "Tom Christie" [email protected] wrote:

Seems reasonable, yeah. How would you feel about a simple Boolean? I'm not
sure if an 'optional' case is really necessary/desirable.

—
Reply to this email directly or view it on GitHubhttps://github.com/tomchristie/django-rest-framework/issues/905#issuecomment-18804272
.

"I solved my own problem temporarily. I just extended the SimpleRouter class
and copy pasted the routes from the original then put question marks at the
end of all the routes."

I will use this solution instead of the trailing_slash boolean (which, if false, insists that a trailing slash is NOT used), as I think it's needlessly frustrating to make user's worry about whether or not to use a trailing slash. What on Earth is the use case for causing a request to fail if it does/does not have a trailing slash?

That is an excellent question. I believe it has something to do with some
standard written solely for APIs.

On Tue, Jul 30, 2013 at 9:12 PM, alexf101 [email protected] wrote:

"I solved my own problem temporarily. I just extended the SimpleRouter
class
and copy pasted the routes from the original then put question marks at the
end of all the routes."

I will use this solution instead of the trailing_slash boolean (which, if
false, insists that a trailing slash is NOT used), as I think it's
needlessly frustrating to make user's worry about whether or not to use a
trailing slash. What on Earth is the use case for causing a request to fail
if it does/does not have a trailing slash?

—
Reply to this email directly or view it on GitHubhttps://github.com/tomchristie/django-rest-framework/issues/905#issuecomment-21783916
.

I am subclassing DefaultRouter and setting self.trailing_slash = "/?" during the init. This works for the list views, but not for the detail views. The detail views return a 404 if a format suffix is in place.

As bobobo1618 said, literally copy and paste the list of routers, and replace everything there. It worked for me (I don't know why your solution didn't work for you).

On the Angular side you can use a request interceptor to add the trailing slash back in as needed.

If you want to accept both a trailing slash or no trailing slash, we have found this to work so far. (Though it has not been tested much yet.)

## DOCUMENTAION EXAMPLE with noted additions.
from django.conf.urls import patterns, url, include
from rest_framework import routers
from quickstart import views

router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet)
router.register(r'groups', views.GroupViewSet)

# First addition
# Now setup a router that does not require a trailing slash
slashless_router = routers.DefaultRouter(trailing_slash=False)
slashless_router.registry = router.registry[:]

urlpatterns = patterns('',
    url(r'^', include(router.urls)),
    # Second addition
    url(r'^', include(slashless_router.urls)),
)

Excellent solution :) so simple
On Mar 12, 2014 7:57 AM, "Ben Spaulding" [email protected] wrote:

If you want to accept both a trailing slash or no trailing slash, we
have found this to work so far. (Though it has not been tested much yet.)

DOCUMENTAION EXAMPLE with noted additions.

from django.conf.urls import patterns, url, include
from rest_framework import routers
from quickstart import views

router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet)
router.register(r'groups', views.GroupViewSet)

First addition

Now setup a router that does not require a trailing slash

slashless_router = routers.DefaultRouter(trailing_slash=False)
slashless_router.registry = router.registry[:]

urlpatterns = patterns('',
url(r'^', include(router.urls)),
# Second addition
url(r'^', include(slashless_router.urls)),
)

Reply to this email directly or view it on GitHubhttps://github.com/tomchristie/django-rest-framework/issues/905#issuecomment-37417867
.

@alexf101 @benspaulding this solution will lead to duplication in the breadcrums in the browsable API. If this is ok for you, you can use it :)

I like the changed regex in Default/SimpleRouter more.

An update to what I said a few months back...

I am subclassing DefaultRouter and setting self.trailing_slash = "/?" during the init.

You will also need to override get_lookup_regex as the patch does not completely work for this special case, so you can get detail views to work with this snippet that's modified from https://github.com/tomchristie/django-rest-framework/commit/005f475c6af023cc7c75cf38d3a89e22638e5d84:

def get_lookup_regex(self, viewset):
    """                                                                     
    Given a viewset, return the portion of URL regex that is used           
    to match against a single instance.                                     
    """

    # Don't consume `.json` style suffixes                                  
    base_regex = '(?P<{lookup_field}>[^/.]+)'
    lookup_field = getattr(viewset, 'lookup_field', 'pk')

    return base_regex.format(lookup_field=lookup_field)

This also does not cause issues within the browsable API.

@syphar Thanks for pointing that out. We are not currently using the browsable API, so we didn’t notice it, but we are likely to in the future. Good to know.

@kevin-brown Oh really? I’ll have to try again, then, because I could not get your solution to work. Thanks!

FWIW, the fix for this issue has just landed in Angular: https://github.com/angular/angular.js/commit/3878be52f6d95fca4c386d4a5523f3c8fcb04270. The commit message contains info how to configure it.

ember-data doesn't use trailing slashes by default: http://emberjs.com/guides/models/the-rest-adapter/#toc_url-conventions

Is there a config option to allow the absence of trailing slashes yet in django-rest-framework?

@kevin-brown I'm having trouble getting your solution to work. I've tried with and without overriding get_lookup_regex.

class MyAPIRouter(routers.SimpleRouter):
    trailing_slash = "/?"

Nevermind, I should have just read the documentation. http://www.django-rest-framework.org/api-guide/routers.html#simplerouter

router = routers.SimpleRouter(trailing_slash=False)

I've been testing with the trailing_slash=False option and is working smoothly until now.

Based on the comments here i ended up doing something like this:

class OptionalSlashDefaultRouter(DefaultRouter):
    def __init__(self, *args, **kwargs):
        self.trailing_slash = '/?'

        if 'root_renderers' in kwargs:
            self.root_renderers = kwargs.pop('root_renderers')
        else:
            self.root_renderers = list(api_settings.DEFAULT_RENDERER_CLASSES)

        super(SimpleRouter, self).__init__()


router = OptionalSlashDefaultRouter()

@psychok7 does follow class cause any problems?

class OptionalTrailingSlashRouter(routers.DefaultRouter):
    def __init__(self, *args, **kwargs):
        super(OptionalTrailingSlashRouter, self).__init__(*args, **kwargs)
        self.trailing_slash = '/?'

@adeelyounas i don't think i have tried that one, but i guess it shouldn't. Please let us know if it works properly.

@psychok7 I did not find any issues so far but will share if I find one.

Complete solution with Django 1.11

from django.conf.urls import include, url
from rest_framework.routers import DefaultRouter

from api.v1.views import BookViewSet

router = DefaultRouter()
slashless_router = DefaultRouter(trailing_slash=False)
slashless_router.registry = router.registry[:]
router.register(r'^books', BookViewSet, base_name='books')

urlpatterns = [
    url(r'^', include(router.urls)),
    url(r'^', include(slashless_router.urls)),
]

Was this page helpful?
0 / 5 - 0 ratings