From 2c5af63d0bd561af5652d39cea8b5c5a9dcd5348 Mon Sep 17 00:00:00 2001 From: Federico Caselli Date: Sun, 29 Sep 2024 13:51:33 +0200 Subject: [PATCH] fix: take into account async only handlers --- falcon/app_helpers.py | 9 ++++-- falcon/http_error.py | 1 + tests/test_httperror.py | 70 ++++++++++++++++++++++++++++++----------- 3 files changed, 60 insertions(+), 20 deletions(-) diff --git a/falcon/app_helpers.py b/falcon/app_helpers.py index 224e0b6c6..0f9893288 100644 --- a/falcon/app_helpers.py +++ b/falcon/app_helpers.py @@ -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: @@ -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() diff --git a/falcon/http_error.py b/falcon/http_error.py index c18b49d4d..508bd2524 100644 --- a/falcon/http_error.py +++ b/falcon/http_error.py @@ -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: diff --git a/tests/test_httperror.py b/tests/test_httperror.py index 78be3e103..68b09c45d 100644 --- a/tests/test_httperror.py +++ b/tests/test_httperror.py @@ -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 @@ -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'410 Gone' ) -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): @@ -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): @@ -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()