Skip to content

Do not treat missing non-form data as empty dict #7199

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions docs/api-guide/requests.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ REST framework's Request objects provide flexible request parsing that allows yo
* It includes all parsed content, including *file and non-file* inputs.
* It supports parsing the content of HTTP methods other than `POST`, meaning that you can access the content of `PUT` and `PATCH` requests.
* It supports REST framework's flexible request parsing, rather than just supporting form data. For example you can handle incoming [JSON data] similarly to how you handle incoming [form data].
* If the client does not send any data and does not specify form encoding, the value of `.data` is determined by the `DEFAULT_MISSING_DATA` setting. If form encoding is used and no data is sent, `.data` will be an empty Django `QueryDict`.

For more details see the [parsers documentation].

Expand Down
7 changes: 7 additions & 0 deletions docs/api-guide/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ A view inspector class that will be used for schema generation.

Default: `'rest_framework.schemas.openapi.AutoSchema'`

#### DEFAULT_MISSING_DATA
Copy link
Member

Choose a reason for hiding this comment

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

Maybe let's call this something like EMPTY_REQUEST_DATA?

The DEFAULT_XYZ cases make sense because they're things that can be overridden, and are populating the default value. In this case it's not a default case that can be overridden, but rather the base case that it always used if there's an empty request.

Copy link
Member

Choose a reason for hiding this comment

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

Happy to take feedback on alternate naming choices, too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I chose "missing" as opposed to "empty" because conventional usage of the word "empty" would suggest that a request body of "", [] or {} is also empty (len(request.data) == 0 after deserialization). I feared that if we call it "EMPTY_DATA", one may think that any "empty-like" payload would be coerced into this value. -- However, I don't feel strongly, as long as the documentation is clear.

Regarding whether it is a default: If by "override", you mean overriding by view attribute or similar, fair enough; one could also argue that the client can override it (by sending data), which would warrant the name DEFAULT_DATA or DEFAULT_REQUEST_DATA.

(Also, it's conceivable that there would a view attribute to override this setting at some point, although right now the request would not have the corresponding context.)

Having said all this, I think my favorite would be DEFAULT_REQUEST_DATA, as I think it is accurate and future-proof. But in the end, I don't have any stakes in the naming, and will be ok with whatever you prefer.


The value that should be used for `request.data` when the client did not send any data in the request body. This
setting applies only if the client did not send a header indicating form encoding.

Default: `None`

---

## Generic view settings
Expand Down
2 changes: 1 addition & 1 deletion rest_framework/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ def _parse(self):
if media_type and is_form_media_type(media_type):
empty_data = QueryDict('', encoding=self._request._encoding)
else:
empty_data = {}
empty_data = api_settings.DEFAULT_MISSING_DATA
Copy link
Member

Choose a reason for hiding this comment

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

instead of exposing a new API can we just handle the empty data as None value instead of dict?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That was my initial proposal, and I have no objection to doing so. However, please see the discussion in #7195 (comment) (and the context that led up to this post).

Copy link
Member

Choose a reason for hiding this comment

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

what I am suggesting you is based on this comment #7199 (comment) as Tom is reluctant to introduce new API surface to the framework. I saw those discussions as well and do agree with most of them.

Copy link
Contributor Author

@peterthomassen peterthomassen Nov 28, 2022

Choose a reason for hiding this comment

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

Yes, but Tom was even more reluctant to silently change the behavior ("too big of a behavior change" in this earlier comment). He seemed to be more ok with adding the setting, see #7195 (comment).

I have no stakes, and either way is fine for me. It's just think that this question needs clarification between Tom and you, and I can't answer it.

Copy link
Member

Choose a reason for hiding this comment

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

@tomchristie need your input here

Copy link
Member

Choose a reason for hiding this comment

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

should we try empty_data = {} or None? and get rid of a new settings?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't understand how we could "try" empty_data = {} as that is the current situation. As for trying None, that breaks existing tests, which is a non-starter.

Tom has already given his input and agreed to the above code change, see his thumbs-up on #7195 (comment) and the next comment, #7195 (comment).

empty_files = MultiValueDict()
return (empty_data, empty_files)

Expand Down
1 change: 1 addition & 0 deletions rest_framework/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation',
'DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata',
'DEFAULT_VERSIONING_CLASS': None,
'DEFAULT_MISSING_DATA': None,

# Generic view behavior
'DEFAULT_PAGINATION_CLASS': None,
Expand Down
4 changes: 2 additions & 2 deletions tests/test_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,14 @@ def test_standard_behaviour_determines_no_content_GET(self):
Ensure request.data returns empty QueryDict for GET request.
"""
request = Request(factory.get('/'))
assert request.data == {}
assert request.data is None

def test_standard_behaviour_determines_no_content_HEAD(self):
"""
Ensure request.data returns empty QueryDict for HEAD request.
"""
request = Request(factory.head('/'))
assert request.data == {}
assert request.data is None

def test_request_DATA_with_form_content(self):
"""
Expand Down
22 changes: 21 additions & 1 deletion tests/test_testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class BasicSerializer(serializers.Serializer):
@api_view(['POST'])
def post_view(request):
serializer = BasicSerializer(data=request.data)
serializer.allow_null = ('allow_null' in request.query_params)
serializer.is_valid(raise_exception=True)
return Response(serializer.validated_data)

Expand Down Expand Up @@ -191,7 +192,26 @@ def test_invalid_multipart_data(self):
path='/view/', data={'valid': 123, 'invalid': {'a': 123}}
)

def test_empty_post_uses_default_boolean_value(self):
def test_missing_post_payload_causes_400(self):
response = self.client.post(
'/post-view/',
data=None,
content_type='application/json'
)
assert response.status_code == 400
assert response.data['non_field_errors'] == ['No data provided']

def test_missing_post_payload_allow_null_causes_200(self):
response = self.client.post(
'/post-view/?allow_null=1',
data=None,
content_type='application/json'
)
assert response.status_code == 200
assert response.data is None

@override_settings(REST_FRAMEWORK={'DEFAULT_MISSING_DATA': {}})
def test_missing_post_payload_coerced_dict_uses_default_boolean_value(self):
response = self.client.post(
'/post-view/',
data=None,
Expand Down
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy