Skip to content
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions draft/0015-content-types.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
DEP 0015: Content type aware parsing and modernization of the HttpRequest API.

DEP

0015

Author

David Smith

Implementation Team

TBC

Shepherd

TBC

Status

Draft

Type

Feature

Created

2024-03-25

Last-Modified

2024-05-29

Table of Contents


Abstract
========

Currently Django can parse requests for ``application/x-www-form-urlencoded`` and ``multipart/form-data`` types. Other types, such as JSON are currently returned as a string.

This DEP proposes to add configurable content type parsers to allow parsing of additional content types. It is proposed that Django will include a parsing of JSON, and appriate hooks to allow users to add custom parsers for other content types.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This DEP proposes to add configurable content type parsers to allow parsing of additional content types. It is proposed that Django will include a parsing of JSON, and appriate hooks to allow users to add custom parsers for other content types.
This DEP proposes to add configurable content type parsers to allow parsing of additional content types. It is proposed that Django will include a parsing of JSON, and appropriate hooks to allow users to add custom parsers for other content types.


Parsed data from an ``HttpRequest`` is accessed via its ``POST`` attribute. It would be a breaking change if Django were to start parsing content types where currently a string is returned. To avoid introducing a breaking change it is proposed that a new ``data`` attribute is added for the new behaviour.

While introducing a new name for ``POST`` it is proposed that the names for the other attributes are modernized with an equivalent behaviour.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally proposed request.form_data as the new lowercase name for request.POST back in Ticket 32259, I’d like that we still keep that. We can’t find-and-replace request.POST with request.data without adding new functionality unsafely.

Also, let’s list the renames right here in the abstract, so they’re easy for future readers to find:

Suggested change
While introducing a new name for ``POST`` it is proposed that the names for the other attributes are modernized with an equivalent behaviour.
While introducing a new name for ``POST`` it is proposed that the names for the other attributes are modernized with an equivalent behaviour:
* ``GET`` -> ``query_params``
* ``POST`` -> ``form_data``
* ``COOKIES`` -> ``cookies`
* ``META`` -> ``meta``
* ``FILES`` -> ``files``

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think form_data is confusing.

In the first place, GET forms are a thing. But once you're parsing other content types from the body, there's no form even in play at all.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

post_form_data ? I would just like another lowercase name for the existing attribute so it’s not left uppercase-only, requiring users to adopt data.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, you mean as well as then adding the data attribute? I'd misunderstood.

OK, yes, something like that makes sense.

Let's discuss in Vigo, where we can likely bikeshed it to death over less than a single coffee ☕️

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having spoken with @adamchainz at DjangoCon, I agree with him. Adding form_data as an alias to POST, maintaining the existing behaviour is a good idea. 👍

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tim-schilling I think this is the key point:

We can’t find-and-replace request.POST with request.data without adding new functionality unsafely.

The new .data attribute isn't behaviourally neutral. We need a safe migration path for existing code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for that Carlton. That makes total sense.

However, I think we're trading sources of confusion by adding form_data. I think it's totally possible for a newer dev to misunderstand the differences between request.form_data and a django.forms.Form's data and cleaned_data. We'd need them to know that form_data is the data you pass into the Form class. It's not your form's actual data to be used (cleaned_data)

def view(request):
    if request.method == "POST":
        form = MyForm(request.form_data)
        if form.is_valid():
            some_value = form.cleaned_data["my_field"]

Copy link

@collinanderson collinanderson Apr 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...naming this something closer to the HTTP spec...

The content-type is either application/x-www-form-urlencoded or multipart/form-data so request.form_data is pretty close, right? If we try to be sure to always refer to it as request.form_data, not just form_data, that might help. A full name could be "request body form data".

Edit: request.post_form_data is also probably fine and more clear, because in html forms you can't use method="PUT" or anything else. (or request.body_form_data??)

...differences between request.form_data and a django.forms.Form's data...

  • In normal cases request.form_data and form.data will be the same thing, right?

(I still personally think that application/json should end up as request.json, like how requests' response.json() works. Same with response.json() in javascript's fetch(). I think it would be more clear than an intentionally generic request.data which could be confusing.)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

request.json is also found in Pyramid, alongside request.body (bytes) and others.

Copy link
Member

@tim-schilling tim-schilling Apr 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In normal cases request.form_data and form.data will be the same thing, right?

Yes that's true. I suppose the fallout of this confusion is smaller than I originally thought. Good call out.


The existing ``GET``, ``POST``, ``META``, and ``FILES`` attributes will be maintained for backwards compatibility, and the behaviour (specifically of POST) will remain unchanged.

Specification
=============

Django shall provide parsers for ``application/x-www-form-urlencoded``, ``multipart/form-data`` and ``application/json`` content types.

A parser should be a class which is instantiated with a request and have two methods.

- ``can_handle()`` should accept one argument being the ``media_type`` attempting to be parsed and returns a boolean to indicate if this parser can parse the given media type.
- ``parse()`` accepts one argument being the ``data`` to parse and returns the parsed data.

By default all requests shall be parsed by Django's default parsers. But to ensure backward compatability the new behaviour shall only be available when accessing the parsed response via the new ``data`` attribute.

The new ``data`` attribute should parse requests in as close a way as possible to the current ``POST`` attribute. In addition it shall:

* Parse ``application/JSON....`` types.
* For ``multipart/form-data`` types each part will be parsed with the appropriate parser, if available.
* Raise an ``UnsupportedMediaType`` (415) error if an unsupported content type is attempted to be parsed.

The new data attribute is therefore not 100% equivalent to POST since it will parse JSON (and other data types as parsers for those are configured) where POST would return a string.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just reread the DEP due to a thread on the forum. Is this actually true? While it is true that the new .data attribute is not 100% equivalent it doesn't seem true that POST would ever return a string but rather an empty QueryDict if the content type is not something that it supports.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's probably talking about JSON multiparts (but I'd need to look at tests to recall 100%)


Custom parsers for additional content types shall be supported. To allow this it is proposed that ``HttpRequest`` adds a new property which returns a list of parsers to be used when attempting to parse a request.
The list of parsers to be used can be set on the request, but must be done before ``data`` or ``FILES`` is accessed. This could be done in a middleware or in a view. For example::

def index(request):
request.parsers = [MyCustomParser(), FormParser(), ...]
...
Comment on lines +57 to +62
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In django/django#17546 (comment) it's decided not to have a setting for this. I think we should preserve that decision and rationale in this DEP too.


To mitigate backward compatability concerns the new behaviour shall be accessed using the new ``data`` attribute. This introduces new, lower case names which better reflect the behaviour of the function.
At the same time it is proposed that the following attributes shall be added to modernise the other names:

* query_params (for GET)
* files (for FILES)
* meta (for META)
* cookies (for COOKIES)

The behaviour of renamed attributes shall be 100% compatible with the existing attributes.

# TODO

A separate [ticket 17235](https://code.djangoproject.com/ticket/17235) is accepted which proposes to leave FILES as immutable.
Do we need to mention this here? The new behaviour could be on the lower cased name only?

Motivation
==========

This DEP is needed to add configurable content type parsing to Django. While this is likely to be well received the proposal also suggests adding new aliases which is more controversial.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like that the DEP just assumes that configurable content type parsing is desirable. I think the reasons why we want this should be much more spelled out in this section.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This DEP is needed to add configurable content type parsing to Django. While this is likely to be well received the proposal also suggests adding new aliases which is more controversial.
This DEP is needed to add configurable content type parsing to Django. Since Django takes a batteries included approach, supporting content type parsing for common content types such as ``application/x-www-form-urlencoded``, ``multipart/form-data`` and ``application/json`` is expected for a modern web application framework. It should be configurable so that the community can extend the content types that Django applications can support. This allows the eco-system the flexibility to grow with the rest of web development industry.
While this is likely to be well received the proposal also suggests adding new aliases which is more controversial.


The motivation to improve the names is that ``request.GET`` and ``request.POST`` are misleadingly named:

* ``GET`` contains the URL parameters and is therefore available whatever the request method. This often confuses beginners and "returners" alike.

* ``POST`` contains form data on ``POST`` requests, but not other kinds of data from ``POST`` requests. It can confuse users who are posting JSON or other formats.

Additionally both names can lead users to think e.g. "if request.GET:" means "if this is a GET request", which is not true.

The CAPITALIZED naming style is similar to PHP's global variables $_GET, $_POST, $_FILES etc. (https://www.php.net/manual/en/reserved.variables.get.php ). It stands out as unpythonic, since these are instance variables and not module-level constants (as per PEP8 https://www.python.org/dev/peps/pep-0008/#constants).

However, with ``HttpRequest`` being such a core part of Django renaming these will cause a large amount of churn. The change to the documentation will be significant and many existing tutorials, blog posts and books by authors in the community would require updating to reflect the new, recommended appraoch.
As such it is proposed that the new names are not immediately deprecated.

# TODO

What would we like to say about a deprecation path? What would have to be true for it to even be considered?

See mailing list conversation [1]

[1] https://groups.google.com/g/django-developers/c/Kx8BfU-z4_E/m/gJBuGeZTBwAJ

Rationale
=========

The main objection received by the community is the renaming of the attributes. This causes a lot of churn in documentation to rename attributes where the behaviour of these is equivielent.

Other options are:

- Leave additional content type parsing to 3rd party packages, e.g. DRF
- Introduce content type parsing and only add the new ``data`` attribute.

The new names for unchanged attributes is proposed as it's considered this a worthwhile improvement in its own right and introduces consistent naming across ``HttpRequest`` attributes. That is, without renaming the change only the new ``data`` attribute would be an outlier.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can also add a note that django-upgrade could automate refactoring code to use the new lowercased attributes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initial work showing this is possible: smithdc1/django-upgrade@c043761

Copy link
Member

@tim-schilling tim-schilling Apr 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
A setting was considered for managing the content parsers for a Django application. The setting approach was dismissed because configuring the parsers is seen as something rare. It would be similar to the `FILE_UPLOAD_HANDLERS` setting which is rarely changed from the default. The solution is to have the application configure the parsers in the view or early in the middleware.

@LilyFoote Regarding your comment here: https://github.com/django/deps/pull/88/files#r2041206276

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good, thanks!

I think the wording can be improved in one place - it talks about configuring parsers being rare, but then compares to FILE_UPLOAD_HANDLERS being frequently changed from the default. I think I get what was intended here, but rewording would make it easier to understand.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops! Good catch. Fixed in the suggestion to say "setting which is rarely changed"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works for me now! 🚀

Backwards Compatibility
=======================

This DEP is designed to be backward compatible. The existing ``GET``, ``POST``, ``META``, and ``FILES`` attributes will be maintained for backwards compatibility, and (to emphasise again) the behaviour (specifically of POST) will remain unchanged.

This is similar to the way the headers property was added, whilst maintaining the older dictionary style lookup.

Reference Implementation
========================

There are currently two PRs which are work towards implementation of this DEP.

* Addition of content type parsing https://github.com/django/django/pull/17546
* Modernization of Request Object attribute names https://github.com/django/django/pull/17624

Copyright
=========

This document has been placed in the public domain per the Creative Commons CC0 1.0 Universal license (http://creativecommons.org/publicdomain/zero/1.0/deed).