diff --git a/falcon/app_helpers.py b/falcon/app_helpers.py index 255f73b71..d22eccba6 100644 --- a/falcon/app_helpers.py +++ b/falcon/app_helpers.py @@ -14,8 +14,10 @@ """Utilities for the App class.""" +from collections import OrderedDict from inspect import iscoroutinefunction from typing import IO, Iterable, List, Tuple +from typing import Optional from falcon import util from falcon.constants import MEDIA_JSON @@ -226,14 +228,14 @@ def default_serialize_error(req: Request, resp: Response, exception: HTTPError): resp: Instance of ``falcon.Response`` exception: Instance of ``falcon.HTTPError`` """ - preferred = _negotiate_preffered_media_type(req) + preferred = _negotiate_preffered_media_type(req, resp) if preferred is not None: - if preferred == MEDIA_JSON: - handler, _, _ = resp.options.media_handlers._resolve( - MEDIA_JSON, MEDIA_JSON, raise_not_found=False - ) - resp.data = exception.to_json(handler) + handler, _, _ = resp.options.media_handlers._resolve( + preferred, MEDIA_JSON, raise_not_found=False + ) + if handler: + resp.data = handler.serialize(exception.to_dict(OrderedDict), preferred) else: resp.data = exception.to_xml() @@ -244,8 +246,10 @@ def default_serialize_error(req: Request, resp: Response, exception: HTTPError): resp.append_header('Vary', 'Accept') -def _negotiate_preffered_media_type(req: Request) -> str: - preferred = req.client_prefers((MEDIA_XML, 'text/xml', MEDIA_JSON)) +def _negotiate_preffered_media_type(req: Request, resp: Response) -> Optional[str]: + supported = {MEDIA_XML, 'text/xml', MEDIA_JSON} + supported.update(set(resp.options.media_handlers.keys())) + preferred = req.client_prefers(supported) if preferred is None: # NOTE(kgriffs): See if the client expects a custom media # type based on something Falcon supports. Returning something diff --git a/tests/test_app_helpers.py b/tests/test_app_helpers.py index e9c18ca11..31b8ad098 100644 --- a/tests/test_app_helpers.py +++ b/tests/test_app_helpers.py @@ -1,12 +1,16 @@ import pytest from falcon import HTTPNotFound +from falcon import ResponseOptions from falcon.app_helpers import default_serialize_error +from falcon.media import BaseHandler +from falcon.media import Handlers from falcon.request import Request from falcon.response import Response from falcon.testing import create_environ -JSON = ('application/json', 'application/json', b'{"title": "404 Not Found"}') +JSON_CONTENT = b'{"title": "404 Not Found"}' +JSON = ('application/json', 'application/json', JSON_CONTENT) XML = ( 'application/xml', 'application/xml', @@ -15,7 +19,7 @@ b'404 Not Found' ), ) -CUSTOM_JSON = ('custom/any+json', 'application/json', b'{"title": "404 Not Found"}') +CUSTOM_JSON = ('custom/any+json', 'application/json', JSON_CONTENT) CUSTOM_XML = ( 'custom/any+xml', @@ -26,8 +30,30 @@ ), ) +YAML = ( + 'application/yaml', + 'application/yaml', + (b'error:\n' b' title: 404 Not Found'), +) + + +class FakeYamlMediaHandler(BaseHandler): + def serialize(self, media: object, content_type: str) -> bytes: + return b'error:\n' b' title: 404 Not Found' + class TestDefaultSerializeError: + def test_if_no_content_type_and_accept_fall_back_to_json(self) -> None: + response = Response() + default_serialize_error( + Request(env=(create_environ())), + response, + HTTPNotFound(), + ) + assert response.content_type == 'application/json' + assert response.headers['vary'] == 'Accept' + assert response.data == JSON_CONTENT + @pytest.mark.parametrize( 'accept, content_type, data', ( @@ -35,12 +61,17 @@ class TestDefaultSerializeError: XML, CUSTOM_JSON, CUSTOM_XML, + YAML, ), ) - def test_serializes_error_to_preffered_by_sender( + def test_serializes_error_to_preferred_by_sender( self, accept, content_type, data ) -> None: - response = Response() + handlers = Handlers() + handlers['application/yaml'] = FakeYamlMediaHandler() + options = ResponseOptions() + options.media_handlers = handlers + response = Response(options=options) default_serialize_error( Request(env=(create_environ(headers={'accept': accept}))), response, diff --git a/tests/test_media_handlers.py b/tests/test_media_handlers.py index 8b95600b6..ef078e148 100644 --- a/tests/test_media_handlers.py +++ b/tests/test_media_handlers.py @@ -353,6 +353,20 @@ def on_get(self, req, resp): assert result.json == falcon.HTTPForbidden().to_dict() +def test_handlers_include_new_media_handlers_in_resolving() -> None: + class FakeHandler: + ... + + handlers = media.Handlers({falcon.MEDIA_URLENCODED: media.URLEncodedFormHandler()}) + handler = FakeHandler() + handlers['application/yaml'] = handler + resolved, _, _ = handlers._resolve( + 'application/yaml', 'application/json', raise_not_found=False + ) + assert resolved.__class__.__name__ == handler.__class__.__name__ + assert resolved == handler + + class TestBaseHandler: def test_defaultError(self): h = media.BaseHandler()