Skip to content

move fix for no_content responses to resource layer #179

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

Merged
merged 3 commits into from
Jul 22, 2025
Merged
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
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.34.0
current_version = 0.34.1

[bumpversion:file:pyproject.toml]

Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

# [Unreleased]

# [0.34.1]
- [PR 179](https://github.com/salesforce/django-declarative-apis/pull/179) Move the 204 response handling fix to resources.

# [0.34.0]
- [PR 178](https://github.com/salesforce/django-declarative-apis/pull/178) Fix JSONEmitter for empty response bodies.

Expand Down
7 changes: 1 addition & 6 deletions django_declarative_apis/resources/emitters.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,19 +183,14 @@ def decode(self, data):
def render(self, request):
cb = request.GET.get("callback", None)
assert cb is None, "JSONP Callbacks not supported"
seria = self.decode(self.construct())
if isinstance(seria, list):
if len(seria) == 0 or (len(seria) == 1 and len(seria[0]) == 0):
# the body is empty, no need to run json.dumps
return ""

# Callback
# TODO: do we care about JSONP?
# if cb and is_valid_jsonp_callback_value(cb):
# return '%s(%s)' % (cb, seria)

return json.dumps(
seria,
self.decode(self.construct()),
cls=DjangoJSONEncoder,
ensure_ascii=False,
indent=4,
Expand Down
4 changes: 4 additions & 0 deletions django_declarative_apis/resources/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,10 @@ def __call__(self, request, *args, **kwargs): # noqa: C901
srl = emitter(result, handler, anonymous)

try:
# If the status code is 204, we need to return an empty response. skip the emitter.
if status_code == http.HTTPStatus.NO_CONTENT:
return HttpResponse(b"", status=http.HTTPStatus.NO_CONTENT)

"""
Decide whether or not we want a generator here,
or we just want to buffer up the entire result
Expand Down
2 changes: 1 addition & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
# built documents.

# The full version, including alpha/beta/rc tags.
release = "0.34.0" # set by bumpversion
release = "0.34.1" # set by bumpversion

# The short X.Y version.
version = release.rsplit(".", 1)[0]
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "django-declarative-apis"
version = "0.34.0" # set by bumpversion
version = "0.34.1" # set by bumpversion
description = "Simple, readable, declarative APIs for Django"
readme = "README.md"
dependencies = [
Expand Down
9 changes: 0 additions & 9 deletions tests/resources/test_emitters.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,15 +109,6 @@ def test_decode(self):
resp = em.render(django.test.RequestFactory().get("/"))
self.assertEqual(json.loads(resp), ["foo", "bar"])

def test_decode_empty_list(self):
em = emitters.JSONEmitter([""], lambda: None)
resp = em.render(django.test.RequestFactory().get("/"))
self.assertEqual(resp, "")

em = emitters.JSONEmitter([], lambda: None)
resp = em.render(django.test.RequestFactory().get("/"))
self.assertEqual(resp, "")


class DjangoEmitterTestCase(unittest.TestCase):
def test_render_http_response_succes(self):
Expand Down
21 changes: 21 additions & 0 deletions tests/resources/test_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import django.test
from django.test.utils import override_settings
from unittest import mock
from django.http import HttpResponse

from django_declarative_apis.authentication.oauthlib import oauth_errors
from django_declarative_apis.resources import resource
Expand Down Expand Up @@ -212,3 +213,23 @@ def test_email_exception(self):
(subj, body, _, __), ___ = mock_email.call_args_list[0]
self.assertEqual(subj, "[Django] Django Declarative APIs crash report")
self.assertEqual(body, traceback)

def test_call_no_content_response(self):
"""Test that HTTP 204 responses return empty content with content-length 0"""

def handle_delete(request, *args, **kwargs):
# Handler returns an HttpResponse with 204 status as the result
return http.HTTPStatus.OK, HttpResponse(status=http.HTTPStatus.NO_CONTENT)

class Handler:
allowed_methods = ("DELETE",)
method_handlers = {"DELETE": handle_delete}

req = self.create_request(method="DELETE")
res = resource.Resource(lambda: Handler())
resp = res(req)

# Verify the response has 204 status and empty content
self.assertEqual(resp.status_code, http.HTTPStatus.NO_CONTENT)
self.assertEqual(resp.content, b"")
self.assertEqual(len(resp.content), 0)