Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Implement support for X-HTTP-Method-Override in flask.views.MethodView. #582

Closed
wants to merge 1 commit into from

7 participants

Joel Perras Don't Add Me To Your Organization a.k.a The Travis Bot Kenneth Reitz Matt Wright Shawn Adams Armin Ronacher Tom Christie
Joel Perras

When an incoming request contains the X-HTTP-Method-Override header, the
value of that header is used as the view method to be executed instead
of the original incoming HTTP verb.

This helps support dumb(er) webservers and proxies that don't understand newer HTTP verbs (e.g. PATCH), of which Amazon's Elastic Load Balancer service is a member.

I only added the functionality to flask.views.MethodView, because I was unsure about what would be a good way to do it with the standard dispatch_request; developers often introspect the HTTP method inside of decorator-based routes when supporting more than one verb in a route, so should the request.method value be overridden completely, or should devs be required to check for the X-HTTP-Method-Override header themselves? Neither option seems very appealing to me.

Joel Perras jperras Implement support for X-HTTP-Method-Override in flask.views.MethodView.
When an incoming request contains the X-HTTP-Method-Override header, the
value of that header is used as the view method to be executed instead
of the original incoming HTTP verb.
708a2ab
Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request passes (merged 708a2ab into 80b2689).

Kenneth Reitz
Collaborator

+100, I'd love to see this enabled in Flask by default. However, I believe there's a built-in way to do this with werkzeug. @mitsuhiko ?

Matt Wright

I'm wary of HTTP headers that start with X-. I use a WSGI middleware paired with a query string to care of this for me.

class HTTPMethodOverrideMiddleware(object):
    """The HTTPMethodOverrideMiddleware middleware implements the hidden HTTP
    method technique. Not all web browsers support every HTTP method, such as
    DELETE and PUT. Using a querystring parameter is the easiest implementation
    given Werkzeug and how middleware is implemented. The following is an
    example of how to create a form with a PUT method:

        <form action="/stuff/id?__METHOD_OVERRIDE__=PUT" method="POST">
            ...
        </form>
    """
    def __init__(self, app):
        self.app = app

    def __call__(self, environ, start_response):
        if '__METHOD_OVERRIDE__' in environ.get('QUERY_STRING', ''):
            args = url_decode(environ['QUERY_STRING'])
            method = args.get('__METHOD_OVERRIDE__').upper()
            if method in ['GET', 'POST', 'PUT', 'DELETE']:
                method = method.encode('ascii', 'replace')
                environ['REQUEST_METHOD'] = method
        return self.app(environ, start_response)
Joel Perras

@mattupstate Agreed, but there are some situations where X- headers are acceptable; the most notable example being the pseudo-standard X-Requested-With used by werkzeug to determine if the originating request originated from an XMLHttpRequest from the browser.

I do like your middleware solution idea, but I'd rather not pollute my query strings with metadata that is better represented in a header.

Matt Wright

In some cases it is quite convenient to use a query string, especially in plain old HTML views that POST forms. Headers are nice with clients that are more full featured. I once looked into seeing if a header value could be retrieved in the middleware but I think it would have proved difficult since the environ wasn't "parsed" yet but I'm also not very well versed in WSGI yet. I think it would be ideal if it's possible to check for either a query string or header in the middleware.

Joel Perras

Any thoughts, @mitsuhiko?

Shawn Adams

Building on what @mattupstate posted here is an example that looks for an X-HTTP-Method-Override header

class HTTPMethodOverrideMiddleware(object):
    def __init__(self, app):
        self.app = app

    def __call__(self, environ, start_response):
        method = environ.get('HTTP_X_HTTP_METHOD_OVERRIDE', '').upper()
        if method in ['GET', 'POST', 'PUT', 'DELETE']:
            method = method.encode('ascii', 'replace')
            environ['REQUEST_METHOD'] = method
        return self.app(environ, start_response)
Armin Ronacher
Owner

This should indeed be fixed on the WSGI layer for consistency. It's easy enough to make a middleware, maybe we should put it into the docs.

Armin Ronacher mitsuhiko closed this
Tom Christie

The HTTP_X_HTTP_METHOD_OVERRIDE should probably only be respected if the method is POST.
Eg. As per Google API style. https://developers.google.com/gdata/docs/2.0/basics

Using HTTP_X_HTTP_METHOD_OVERRIDE with GET requests could result unexpected behavior due to caching, and I'd also be wary that it might represent a CSRF attack vector in some circumstances.

For further reference, also see @jacobian's note against this similar pull request: django-tastypie/django-tastypie#351

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Aug 27, 2012
  1. Joel Perras

    Implement support for X-HTTP-Method-Override in flask.views.MethodView.

    jperras authored
    When an incoming request contains the X-HTTP-Method-Override header, the
    value of that header is used as the view method to be executed instead
    of the original incoming HTTP verb.
This page is out of date. Refresh to see the latest.
Showing with 26 additions and 0 deletions.
  1. +19 −0 flask/testsuite/views.py
  2. +7 −0 flask/views.py
19 flask/testsuite/views.py
View
@@ -145,6 +145,25 @@ def head(self):
self.assert_equal(rv.data, '')
self.assert_equal(rv.headers['X-Method'], 'HEAD')
+ def test_method_override(self):
+ app = flask.Flask(__name__)
+
+ class Override(flask.views.MethodView):
+ def put(self):
+ return 'PUT'
+
+ def post(self):
+ return 'POST'
+
+ app.add_url_rule('/', view_func=Override.as_view('override'))
+ c = app.test_client()
+ rv = c.post('/')
+ self.assert_equal(rv.data, 'POST')
+ rv = c.put('/')
+ self.assert_equal(rv.data, 'PUT')
+ rv = c.post('/', headers=[('X-HTTP-Method-Override', 'PUT')])
+ self.assert_equal(rv.data, 'PUT')
+
def suite():
suite = unittest.TestSuite()
7 flask/views.py
View
@@ -142,6 +142,13 @@ def post(self):
def dispatch_request(self, *args, **kwargs):
meth = getattr(self, request.method.lower(), None)
+
+ # If the request method has been explicitly overriden, use that
+ # instead of the original HTTP verb.
+ if 'X-HTTP-Method-Override' in request.headers:
+ override = request.headers['X-HTTP-Method-Override']
+ meth = getattr(self, override.lower(), None)
+
# if the request method is HEAD and we don't have a handler for it
# retry with GET
if meth is None and request.method == 'HEAD':
Something went wrong with that request. Please try again.