Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Parsing Unsupported Requests (PUT, DELETE, etc.) in Django

DZone's Guide to

Parsing Unsupported Requests (PUT, DELETE, etc.) in Django

When parsing in Django, issues can arise. Learn how to work around these issues with a few lines of Python and a few queries.

· Web Dev Zone ·
Free Resource

Deploying code to production can be filled with uncertainty. Reduce the risks, and deploy earlier and more often. Download this free guide to learn more. Brought to you in partnership with Rollbar.

Django is a mature web framework for Python. It does its job well, however, anyone writing a RESTful API in Django will soon find that it lacks some of the basic request parsing you would expect from a web framework.

This post is written for Django 1.9 and 1.11.

The Problem

If you have used Django before then you know the request parameters for GET and POSTmethods are parsed by Django automatically and placed inside similarly named properties of the request object.

class HelloController(View):
    def get(self, request):
        hello_param = request.GET["helloParam"]

    def post(self, request):
        hello_param = request.POST["helloParam"]

So far so good, now it’s time to add a little bit of modification to your controller, and being a RESTful API, it’s time to utilize the PUT method.

class HelloController(View):
    def put(self, request):
        hello_param = request.PUT["helloParam"]

You expect this code to work and run it. KaPUT, pun intended, it does not. You will see an error thrown because there’s no attribute named PUT in the request object.

Django only parses the request parameters for GET and POST HTTP methods. The Django team have some good(ish) reasons for this, it’s explained in this post. You also do not get the convenience of accessing any uploaded files via the request.FILES attribute in other HTTP methods like PUT.

Solution

Before you go any further, there are third-party frameworks like Django REST Framework or Tasty Pie that do a decent job of tackling the REST API issues faced by Django. So long as you adhere to their conventions.

However, if you don’t want to use them for some reason, maybe the project is an existing one with a big codebase you don’t want to refactor, maybe you don’t want to follow these framework conventions, or maybe you are prototyping and don’t have time to learn the new frameworks right now, you can use the following solutions.

First Iteration

In order to access our PUT parameters, we need to create our own QueryDict object manually.

put_params = QueryDict(request.body)

No big deal, just a single line of code. But it gets ugly pretty soon, you will have to pepper this all over your views, and then there’s the file issue. Django doesn’t populate the convenient FILES attribute for PUT requests. You have to go through a lot of hassle to gain access to the files sent in a PUT request. Basically, you need to access the upload_handlers of the request and read the file streams, then, of course, you need to parse the parameters other than the files that are probably in the request. HASSLE! HASSLE! HASSLE!

Hmm… maybe there’s something in one of these REST frameworks that can help us.

Look at what we found in Django Piston.

def coerce_put_post(request):
    """
    The try/except abominiation here is due to a bug
    in mod_python. This should fix it.
    """
    if request.method == "PUT":
        # Bug fix: if _load_post_and_files has already been called, for
        # example by middleware accessing request.POST, the below code to
        # pretend the request is a POST instead of a PUT will be too late
        # to make a difference. Also calling _load_post_and_files will result
        # in the following exception:
        #   AttributeError: You cannot set the upload handlers after the upload has been processed.
        # The fix is to check for the presence of the _post field which is set
        # the first time _load_post_and_files is called (both by wsgi.py and
        # modpython.py). If it's set, the request has to be 'reset' to redo
        # the query value parsing in POST mode.
        if hasattr(request, '_post'):
            del request._post
            del request._files

        try:
            request.method = "POST"
            request._load_post_and_files()
            request.method = "PUT"
        except AttributeError:
            request.META['REQUEST_METHOD'] = 'POST'
            request._load_post_and_files()
            request.META['REQUEST_METHOD'] = 'PUT'

        request.PUT = request.POST

When you get past the error handling and workarounds (explained in the comment) in there, what this method does is actually simple. It sets the request.method as POSTtemporarily and triggers the request._load_post_and_files() method. This _load_post_and_files() method is where all the request parameters and file parsing happen in Django, for the POST method.

Once we trick the _load_post_and_files() method into parsing all the request details for us, we then set the method back to PUT.

Django also doesn’t automatically parse JSON based on the content-type of the request.

You need to parse the JSON data manually, like this:

json_params = json.loads(request.body)

Using this code every time we need to get something parsed is not ideal. It’s annoying and looks unclean, not to mention being an affront to the Gods of Clean Code.

Middleware to the Rescue

The solution for our dilemma is called Django middleware. In case you aren’t already familiar with Django middlewares, they are basically hooks that can attach themselves to the request and response processing chain in order to alter them. Check out the official documentation from here if you need more information on how they work.

All we need to do is put these snippets in a Django middleware and we will have easy access to our request data.

import json

from django.http import HttpResponseBadRequest
from django.utils.deprecation import MiddlewareMixin


class PutParsingMiddleware(MiddlewareMixin):
    def process_request(self, request):
        if request.method == "PUT" and request.content_type != "application/json":
            if hasattr(request, '_post'):
                del request._post
                del request._files
            try:
                request.method = "POST"
                request._load_post_and_files()
                request.method = "PUT"
            except AttributeError as e:
                request.META['REQUEST_METHOD'] = 'POST'
                request._load_post_and_files()
                request.META['REQUEST_METHOD'] = 'PUT'

            request.PUT = request.POST


class JSONParsingMiddleware(MiddlewareMixin):
    def process_request(self, request):
        if (request.method == "PUT" or request.method == "POST") and request.content_type == "application/json":
            try:
                request.JSON = json.loads(request.body)
            except ValueError as ve:
                return HttpResponseBadRequest("unable to parse JSON data. Error : {0}".format(ve))

As you can see in the PutParsingMiddleware we parse the PUT method parameters so long as the content_type is not JSON.

We do the JSON data parsing for PUT and POST requests in the JSONParsingMiddleware class and put the parsed data in the request.JSONattribute. We also return a HttpResponseBadRequest response if the JSON data is unparseable.

We can now enjoy the fruits of our labor.

class HelloController(View):
    def post(self, request):
        hello_param = request.JSON["helloParam"]

    def put(self, request):
        hello_param = request.PUT["helloParam"]
        hello_file = request.FILES["helloFile"]

Much cleaner wouldn’t you say?

Deploying code to production can be filled with uncertainty. Reduce the risks, and deploy earlier and more often. Download this free guide to learn more. Brought to you in partnership with Rollbar.

Topics:
django ,request handling ,rest api ,python ,web dev

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}