Why don't Django and CherryPy support HTTP verb-based dispatch natively?
Asked Answered
A

4

12

It's not the same to POST to an URL than to GET it, DELETE it or PUT it. These actions are fundamentally different. However, Django seems to ignore them in its dispatch mechanism. Basically, one is forced to either ignore HTTP verbs completely or do this on every view:

def my_view(request, arg1, arg2):
    if request.method == 'GET':
        return get_view(request, arg1, arg2)
    if request.method == 'POST':
        return post_view(request, arg1, arg2)
    return http.HttpResponseNotAllowed(['GET', 'POST'])

The few solutions I have found for this in the web (this snippet for verb-based dispatch, or this decorator for verb requirement) are not very elegant as they are clearly just workarounds.

The situation with CherryPy seems to be the same. The only frameworks I know of that get this right are web.py and Google App Engine's.

I see this as a serious design flaw for a web framework. Does anyone agree? Or is it a deliberate decision based on reasons/requirements I ignore?

Ashjian answered 10/8, 2009 at 12:39 Comment(0)
S
13

I can't speak for Django, but in CherryPy, you can have one function per HTTP verb with a single config entry:

request.dispatch = cherrypy.dispatch.MethodDispatcher()

However, I have seen some situations where that's not desirable.

One example would be a hard redirect regardless of verb.

Another case is when the majority of your handlers only handle GET. It's especially annoying in that case to have a thousand page handlers all named 'GET'. It's prettier to express that in a decorator than in a function name:

def allow(*methods):
    methods = list(methods)
    if not methods:
        methods = ['GET', 'HEAD']
    elif 'GET' in methods and 'HEAD' not in methods:
        methods.append('HEAD')
    def wrap(f):
        def inner(*args, **kwargs):
            cherrypy.response.headers['Allow'] = ', '.join(methods)
            if cherrypy.request.method not in methods:
                raise cherrypy.HTTPError(405)
            return f(*args, **kwargs):
        inner.exposed = True
        return inner
    return wrap

class Root:
    @allow()
    def index(self):
        return "Hello"

    cowboy_greeting = "Howdy"

    @allow()
    def cowboy(self):
        return self.cowboy_greeting

    @allow('PUT')
    def cowboyup(self, new_greeting=None):
        self.cowboy_greeting = new_greeting

Another common one I see is looking up data corresponding to the resource in a database, which should happen regardless of verb:

def default(self, id, **kwargs):
    # 404 if no such beast
    thing = Things.get(id=id)
    if thing is None:
        raise cherrypy.NotFound()

    # ...and now switch on method
    if cherrypy.request.method == 'GET': ...

CherryPy tries to not make the decision for you, yet makes it easy (a one-liner) if that's what you want.

Stairs answered 10/8, 2009 at 14:22 Comment(0)
S
6

Came across this from Google, and thought of updating.

Django

Just FYI, This is now supported in Django as class based views. You can extend the generic class View and add methods like get(), post(), put() etc. E.g. -

from django.http import HttpResponse
from django.views.generic import View

class MyView(View):

    def get(self, request, *args, **kwargs):
        return HttpResponse('Hello, World!')

The dispatch() part handles this-

dispatch(request, *args, **kwargs)

The view part of the view – the method that accepts a request argument plus arguments, and returns a HTTP response.

The default implementation will inspect the HTTP method and attempt to delegate to a method that matches the HTTP method; a GET will be delegated to get(), a POST to post(), and so on.

By default, a HEAD request will be delegated to get(). If you need to handle HEAD requests in a different way than GET, you can override the head() method. See Supporting other HTTP methods for an example.

The default implementation also sets request, args and kwargs as instance variables, so any method on the view can know the full details of the request that was made to invoke the view.

Then you can use it in urls.py -

from django.conf.urls import patterns, url

from myapp.views import MyView

urlpatterns = patterns('',
    url(r'^mine/$', MyView.as_view(), name='my-view'),
)

More details.

CherryPy

CherryPy now also supports this. They have a full page on this.

Showroom answered 31/3, 2013 at 10:35 Comment(0)
F
2

I believe the decision for django was made because usually just GET and POST is enough, and that keeps the framework simpler for its requirements. It is very convenient to just "not care" about which verb was used.

However, there are plenty other frameworks that can do dispatch based on verb. I like werkzeug, it makes easy to define your own dispatch code, so you can dispatch based on whatever you want, to whatever you want.

Fryd answered 10/8, 2009 at 12:46 Comment(5)
Even though usually GET and POST are enough, the number of verbs is not the issue here. I just can't think of a single use case where the controller (called "view" in Django) should do exactly the same for both GET and POST.Ashjian
GET and POST are very common, I agree. However, I still don't see why the default should be to completely ignore the verb.Ashjian
@martin: why not? they're about the same thing. you can easily write an application that doesn't care if the request came as a get or as a post.Fryd
@nosklo: because they are not the same thing at all. cases where the verb could be safely ignored are very strange (i still fail to see a single one). therefore, these frameworks make the most uncommon case (verb doesn't matter) the default, and the most common case (verb matters) an optional, non-standardized feature. i think it should be the other way around.Ashjian
ignoring the verbs makes for bloated view functions filled with if POST, if GETLaughton
G
1

Because this is not hard to DIY. Just have a dictionary of accepted verbs to functions in each class.

def dispatcher(someObject, request):
    try:
      return someObject.acceptedVerbs[request.method]()
    except:
      return http.HttpResponseNotAllowed(someObject.acceptedVerbs.keys())
Gause answered 10/8, 2009 at 12:58 Comment(4)
This one looks nice. Any info on how to wire that dispatcher into a simple Django app?Ashjian
I have a utilities file that I include on my more specific views files. Each specific file knows which object to use when calling this dispatcher function.Gause
Sorry, I still don't see it clear. How much code (besides your dispatcher function) is necessary to wire an url pattern to a verb-specific handler? Could you add an example?Ashjian
import my_views_utils.py followed later by dispatcher(someHandlerObjectOfYours, request) in the handler function that you define in your urls.py. It's not Rails, but it works just as well.Gause

© 2022 - 2024 — McMap. All rights reserved.