Skip to content
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
13 changes: 10 additions & 3 deletions faust/web/drivers/aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,15 +180,22 @@ def json(
"""
ctype = content_type or "application/json"
payload: Any = _json.dumps(value)
# normal json returns str, orjson returns bytes
# normal json returns str, orjson returns bytes.
# The str path goes through Response(text=...), which makes aiohttp
# append "; charset=utf-8" to the content-type; the bytes path uses
# Response(body=...), which does not -- so set the charset explicitly
# to keep JSON responses consistent regardless of the JSON backend
# (see #557).
if isinstance(payload, bytes):
return self.bytes(
payload,
response = Response(
body=payload,
content_type=ctype,
charset="utf-8",
status=status,
reason=reason,
headers=headers,
)
return cast(base.Response, response)
else:
return self.text(
payload,
Expand Down
55 changes: 36 additions & 19 deletions tests/unit/web/drivers/test_aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,26 +171,43 @@ def test_json__from_str(self, *, web):
assert response is web.text.return_value

def test_json__from_bytes(self, *, web):
web.bytes = Mock(name="web.bytes")
# orjson returns bytes: build a Response directly with an explicit
# charset=utf-8 (Response(body=...) does not add one on its own),
# so the content-type matches the str path -- see #557.
with patch("faust.web.drivers.aiohttp.Response") as Response:
with patch("faust.utils.json.dumps") as dumps:
dumps.return_value = b"foo"
payload = {"foo": "bar"}
response = web.json(
payload,
content_type="application/x-json",
status=101,
reason="bar",
headers={"k": "v"},
)
dumps.assert_called_once_with(payload)
Response.assert_called_once_with(
body=b"foo",
content_type="application/x-json",
charset="utf-8",
status=101,
reason="bar",
headers={"k": "v"},
)
assert response is Response()

def test_json_content_type_has_charset_for_both_backends(self, *, web):
# Regression for #557: whether the JSON backend returns str (stdlib
# json) or bytes (orjson), the response content-type must carry
# charset=utf-8.
with patch("faust.utils.json.dumps") as dumps:
dumps.return_value = b"foo"
payload = {"foo": "bar"}
response = web.json(
payload,
content_type="application/x-json",
status=101,
reason="bar",
headers={"k": "v"},
)
dumps.assert_called_once_with(payload)
web.bytes.assert_called_once_with(
dumps.return_value,
content_type="application/x-json",
status=101,
reason="bar",
headers={"k": "v"},
)
assert response is web.bytes.return_value
dumps.return_value = b'{"a": 1}' # orjson-style bytes
resp = web.json({"a": 1})
assert resp.headers["Content-Type"] == "application/json; charset=utf-8"

dumps.return_value = '{"a": 1}' # stdlib-style str
resp = web.json({"a": 1})
assert resp.headers["Content-Type"] == "application/json; charset=utf-8"

def test_html(self, *, web):
with patch("faust.web.drivers.aiohttp.Response") as Response:
Expand Down
Loading