Skip to content

Commit

Permalink
fix: take into account async only handlers
Browse files Browse the repository at this point in the history
  • Loading branch information
CaselIT committed Sep 29, 2024
1 parent 8fe56a9 commit 2c5af63
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 20 deletions.
9 changes: 7 additions & 2 deletions falcon/app_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ def default_serialize_error(req: Request, resp: Response, exception: HTTPError)
preferred = req.client_prefers(list(resp.options.media_handlers))

if preferred is not None:
handler, _, _ = resp.options.media_handlers._resolve(
handler, serialize_sync, _ = resp.options.media_handlers._resolve(
preferred, MEDIA_JSON, raise_not_found=False
)
if preferred == MEDIA_JSON:
Expand All @@ -325,7 +325,12 @@ def default_serialize_error(req: Request, resp: Response, exception: HTTPError)
# media_handlers.
resp.data = exception.to_json(handler)
elif handler:
resp.data = handler.serialize(exception.to_dict(), preferred)
if serialize_sync:
resp.data = serialize_sync(exception.to_dict(), preferred)
else:
# NOTE(caselit): Let the app serialize the response if there is no sync
# serializer implemented in the handler.
resp.media = exception.to_dict()
else:
resp.data = exception.to_xml()

Expand Down
1 change: 1 addition & 0 deletions falcon/http_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ def to_json(self, handler: Optional[BaseHandler] = None) -> bytes:
obj = self.to_dict()
if handler is None:
handler = _DEFAULT_JSON_HANDLER
# NOTE: the json handler requires the sync serialize interface
return handler.serialize(obj, MEDIA_JSON)

def to_xml(self) -> bytes:
Expand Down
70 changes: 52 additions & 18 deletions tests/test_httperror.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

import falcon
from falcon.constants import MEDIA_JSON
from falcon.constants import MEDIA_XML
from falcon.constants import MEDIA_YAML
from falcon.media import BaseHandler
import falcon.testing as testing

Expand Down Expand Up @@ -951,16 +953,22 @@ def test_kw_only(self):


JSON_CONTENT = b'{"title": "410 Gone"}'
JSON = ('application/json', 'application/json', JSON_CONTENT)
CUSTOM_JSON = ('custom/any+json', 'application/json', JSON_CONTENT)
JSON = (MEDIA_JSON, MEDIA_JSON, JSON_CONTENT)
CUSTOM_JSON = ('custom/any+json', MEDIA_JSON, JSON_CONTENT)

XML_CONTENT = (
b'<?xml version="1.0" encoding="UTF-8"?><error><title>410 Gone</title></error>'
)
XML = ('application/xml', 'application/xml', XML_CONTENT)
CUSTOM_XML = ('custom/any+xml', 'application/xml', XML_CONTENT)

YAML = ('application/yaml', 'application/yaml', (b'title: 410 Gone!'))
XML = (MEDIA_XML, MEDIA_XML, XML_CONTENT)
CUSTOM_XML = ('custom/any+xml', MEDIA_XML, XML_CONTENT)

YAML = (MEDIA_YAML, MEDIA_YAML, (b'title: 410 Gone!'))
ASYNC_ONLY = ('application/only_async', 'application/only_async', b'this is async')
ASYNC_WITH_SYNC = (
'application/async_with_sync',
'application/async_with_sync',
b'this is sync instead',
)


class FakeYamlMediaHandler(BaseHandler):
Expand All @@ -969,6 +977,20 @@ def serialize(self, media: object, content_type: str) -> bytes:
return b'title: 410 Gone!'


class AsyncOnlyMediaHandler(BaseHandler):
async def serialize_async(self, media: object, content_type: str) -> bytes:
assert media == {'title': '410 Gone'}
return b'this is async'


class SyncInterfaceMediaHandler(AsyncOnlyMediaHandler):
def serialize(self, media: object, content_type: str) -> bytes:
assert media == {'title': '410 Gone'}
return b'this is sync instead'

_serialize_sync = serialize


class TestDefaultSerializeError:
@pytest.fixture
def client(self, util, asgi):
Expand All @@ -988,21 +1010,33 @@ def test_defaults_to_json(self, client, has_json_handler):

@pytest.mark.parametrize(
'accept, content_type, content',
(
JSON,
XML,
CUSTOM_JSON,
CUSTOM_XML,
YAML,
),
(JSON, XML, CUSTOM_JSON, CUSTOM_XML, YAML, ASYNC_ONLY, ASYNC_WITH_SYNC),
)
def test_serializes_error_to_preferred_by_sender(
self, accept, content_type, content, client
self, accept, content_type, content, client, asgi
):
client.app.resp_options.media_handlers['application/yaml'] = (
FakeYamlMediaHandler()
client.app.resp_options.media_handlers[MEDIA_YAML] = FakeYamlMediaHandler()
client.app.resp_options.media_handlers[ASYNC_WITH_SYNC[0]] = (
SyncInterfaceMediaHandler()
)
if asgi:
client.app.resp_options.media_handlers[ASYNC_ONLY[0]] = (
AsyncOnlyMediaHandler()
)
res = client.simulate_get(headers={'Accept': accept})
assert res.content_type == content_type
assert res.headers['vary'] == 'Accept'
assert res.content == content
if content_type == ASYNC_ONLY[0] and not asgi:
# media-json is the default content type
assert res.content_type == MEDIA_JSON
assert res.content == b''
else:
assert res.content_type == content_type
assert res.content == content

def test_json_async_only_error(self, util):
app = util.create_app(True)
app.add_route('/', GoneResource())
app.resp_options.media_handlers[MEDIA_JSON] = AsyncOnlyMediaHandler()
client = testing.TestClient(app)
with pytest.raises(NotImplementedError, match='requires the sync interface'):
client.simulate_get()

0 comments on commit 2c5af63

Please sign in to comment.