Skip to content

Commit

Permalink
Use media handlers to serialize errors by accepted content type
Browse files Browse the repository at this point in the history
  • Loading branch information
copalco committed Nov 26, 2023
1 parent e9d5d51 commit c38e21d
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 12 deletions.
20 changes: 12 additions & 8 deletions falcon/app_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
"""Utilities for the App class."""
from __future__ import annotations

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
Expand Down Expand Up @@ -227,14 +229,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)

Check warning on line 232 in falcon/app_helpers.py

View check run for this annotation

Codecov / codecov/patch

falcon/app_helpers.py#L232

Added line #L232 was not covered by tests

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(

Check warning on line 235 in falcon/app_helpers.py

View check run for this annotation

Codecov / codecov/patch

falcon/app_helpers.py#L235

Added line #L235 was not covered by tests
preferred, MEDIA_JSON, raise_not_found=False
)
if handler:
resp.data = handler.serialize(exception.to_dict(OrderedDict), preferred)

Check warning on line 239 in falcon/app_helpers.py

View check run for this annotation

Codecov / codecov/patch

falcon/app_helpers.py#L239

Added line #L239 was not covered by tests
else:
resp.data = exception.to_xml()

Check warning on line 241 in falcon/app_helpers.py

View check run for this annotation

Codecov / codecov/patch

falcon/app_helpers.py#L241

Added line #L241 was not covered by tests

Expand All @@ -245,8 +247,10 @@ def default_serialize_error(req: Request, resp: Response, exception: HTTPError)
resp.append_header('Vary', 'Accept')

Check warning on line 247 in falcon/app_helpers.py

View check run for this annotation

Codecov / codecov/patch

falcon/app_helpers.py#L247

Added line #L247 was not covered by tests


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)

Check warning on line 253 in falcon/app_helpers.py

View check run for this annotation

Codecov / codecov/patch

falcon/app_helpers.py#L251-L253

Added lines #L251 - L253 were not covered by tests
if preferred is None:
# NOTE(kgriffs): See if the client expects a custom media
# type based on something Falcon supports. Returning something
Expand Down
6 changes: 6 additions & 0 deletions falcon/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ def _resolve(
) -> Tuple[Serializer, Optional[Callable], Optional[Callable]]:
...

def __setattr__(self, key: str, value: Serializer) -> None:
...

def __delattr__(self, key: str) -> None:
...


Link = Dict[str, str]

Expand Down
39 changes: 35 additions & 4 deletions tests/test_app_helpers.py
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -15,7 +19,7 @@
b'<error><title>404 Not Found</title></error>'
),
)
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',
Expand All @@ -26,21 +30,48 @@
),
)

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',
(
JSON,
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,
Expand Down
14 changes: 14 additions & 0 deletions tests/test_media_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit c38e21d

Please sign in to comment.