Skip to content
Draft
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
3 changes: 3 additions & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,9 @@ listed below by date of first contribution:
* rikka0612 (ReinerBRO)
* tuanaiseo
* Het Patel (CuriousHet)
* Stepan Pechenkin (StepanUFL)
* Tang Vu (tang-vu)
* Arthur Prioli (arthurprioli)

(et al.)

Expand Down
8 changes: 5 additions & 3 deletions docs/_newsfragments/1026.newandimproved.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
The :func:`~falcon.testing.simulate_request` now suports ``msgpack``
and returns Content-Type as ``MEDIA_MSGPACK`` in a similar way that
was made to JSON parameters.
The test client (:func:`~falcon.testing.simulate_request` and friends) now
accepts a ``msgpack`` keyword argument, analogous to the existing ``json``
one. When provided, the value is serialized as a MessagePack document and
used as the request body, and the ``Content-Type`` header is set to
:attr:`~falcon.MEDIA_MSGPACK`.
3 changes: 2 additions & 1 deletion docs/_newsfragments/2071.newandimproved.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
A new router option, :attr:`~.CompiledRouterOptions.default_to_on_request`, was
A new router option,
:attr:`~falcon.routing.CompiledRouterOptions.default_to_on_request`, was
added to allow resources to provide a default responder via ``on_request()``
(disabled by default). When enabled,
``on_request()`` is used as the default responder for every unimplemented HTTP
Expand Down
12 changes: 6 additions & 6 deletions docs/_newsfragments/2546.newandimproved.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Added a new :meth:`~falcon.Request.get_query_string_as_media` method to
:class:`~falcon.Request` and :class:`~falcon.asgi.Request` that deserializes
the entire query string as media content. This is useful for implementing
OpenAPI 3.2 ``Parameter Object`` with content, where the whole query string
is treated as serialized data (e.g., JSON). The method URL-decodes the query
string and deserializes it using the configured media handlers.
A new :meth:`~falcon.Request.get_query_string_as_media` method was added to
:class:`~falcon.Request`. The method URL-decodes the entire query string and
deserializes it using the configured media handlers. This is useful for
implementing the OpenAPI 3.2 `querystring parameter location
<https://spec.openapis.org/oas/v3.2.0.html#parameter-locations>`__, where
the entire query string is treated as a single serialized value.
7 changes: 4 additions & 3 deletions docs/_newsfragments/2549.newandimproved.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
A new :meth:`~falcon.Request.get_param_as_media` method was added to
:class:`~falcon.Request` that deserializes a single query-string parameter
using the configured media handlers. The helper accepts an optional
``media_type`` (which falls back to the app's ``default_media_type``).
:class:`~falcon.Request`. It deserializes a single query-string parameter
using the configured media handlers, and accepts an optional ``media_type``
that falls back to the app's
:attr:`~falcon.RequestOptions.default_media_type` when unspecified.
2 changes: 1 addition & 1 deletion docs/_newsfragments/2630.misc.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
``falcon.sys`` (an internal re-export of the standard library's
:mod:`sys` module that was added by oversight long ago) is
:mod:`sys` module that was added inadvertently long ago) is
scheduled for removal in Falcon 5.0. Import the standard library's
:mod:`sys` module directly instead.
5 changes: 5 additions & 0 deletions docs/changes/4.3.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,17 @@ Many thanks to all of our talented and stylish contributors for this release!

- `0x1618 <https://github.com/0x1618>`__
- `0xMattB <https://github.com/0xMattB>`__
- `arthurprioli <https://github.com/arthurprioli>`__
- `CaselIT <https://github.com/CaselIT>`__
- `CuriousHet <https://github.com/CuriousHet>`__
- `gespyrop <https://github.com/gespyrop>`__
- `granuels <https://github.com/granuels>`__
- `jap <https://github.com/jap>`__
- `MannXo <https://github.com/MannXo>`__
- `ReinerBRO <https://github.com/ReinerBRO>`__
- `rushevich <https://github.com/rushevich>`__
- `StepanUFL <https://github.com/StepanUFL>`__
- `tang-vu <https://github.com/tang-vu>`__
- `thisisrick25 <https://github.com/thisisrick25>`__
- `toroleapinc <https://github.com/toroleapinc>`__
- `tuanaiseo <https://github.com/tuanaiseo>`__
Expand Down
100 changes: 61 additions & 39 deletions falcon/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -1175,13 +1175,14 @@ def get_query_string_as_media(

This method URL-decodes the query string and then deserializes it
as a media object using the specified media type handler. This is
useful for implementing OpenAPI 3.2 `Parameter Object with content`_
where the entire query string is treated as serialized content.
useful for implementing the OpenAPI 3.2 `querystring parameter
location`_, where the entire query string is treated as a single
serialized value (typically JSON or form-urlencoded).

For example, if the query string is
``%7B%22numbers%22%3A%5B1%2C2%5D%2C%22flag%22%3Anull%7D``, this
method will URL-decode it to ``{"numbers":[1,2],"flag":null}`` and
then deserialize it as JSON (assuming ``media_type`` is set to
then deserialize it as JSON (assuming `media_type` is set to
``'application/json'``)::

# Query string: ?%7B%22numbers%22%3A%5B1%2C2%5D%7D
Expand All @@ -1195,13 +1196,13 @@ def get_query_string_as_media(
let the media handler try to deserialize the empty string and will
return the value returned by the handler or propagate the exception
raised by it. To instead return a different value in case of an
exception by the handler, specify the argument ``default_when_empty``.
exception by the handler, specify the argument `default_when_empty`.

Args:
media_type: Media type to use for deserialization
(e.g., ``'application/json'``). If not specified, the
``default_media_type`` from :class:`falcon.RequestOptions`
will be used (default ``'application/json'``).
media_type: Media type to use for deserialization (e.g.,
``'application/json'``). If ``None``, falls back to the
value of :attr:`~falcon.RequestOptions.default_media_type`
(default ``'application/json'``).

Keyword Args:
default_when_empty: Fallback value to return when there is no
Expand All @@ -1213,12 +1214,10 @@ def get_query_string_as_media(
object: The deserialized media representation of the query string.

Raises:
ValueError: No media handler is configured for the specified
media type.

.. _Parameter Object with content:
https://spec.openapis.org/oas/latest.html#parameter-object-examples
ValueError: No media handler is configured for `media_type`.

.. _querystring parameter location:
https://spec.openapis.org/oas/v3.2.0.html#parameter-locations
"""
if media_type is None:
media_type = self.options.default_media_type
Expand Down Expand Up @@ -2456,13 +2455,21 @@ def get_param_as_json(
the value could not be parsed as JSON.
"""

# NOTE(mannxo): Delegate to the more general get_param_as_media implementation.
return self.get_param_as_media(
name,
media_type=MEDIA_JSON,
required=required,
store=store,
default=default,
param_value = self.get_param(name, required=required)
if param_value is None:
return default

handler, _, _ = self.options.media_handlers._resolve(
MEDIA_JSON, MEDIA_JSON, raise_not_found=False
)
# NOTE(vytas): Fall back to a default JSON handler so that this legacy
# helper keeps working even when the user has unregistered the
# built-in JSON handler.
if handler is None:
handler = _DEFAULT_JSON_HANDLER

return self._deserialize_param_value(
name, param_value, MEDIA_JSON, handler, store
)

def get_param_as_media(
Expand All @@ -2475,15 +2482,21 @@ def get_param_as_media(
) -> Any:
"""Return a query string parameter's value deserialized by a media handler.

This is useful for implementing the OpenAPI Parameter Object's
`content`_ field, where an individual query-string parameter is
itself a serialized media document such as JSON.

Args:
name (str): Parameter name, case-sensitive (e.g., 'payload').

Keyword Args:
media_type (str | None): Media type to use for deserialization. If
``None``, falls back to the app's ``default_media_type``.
required (bool): Set to ``True`` to raise ``HTTPBadRequest``
instead of returning ``None`` when the parameter is not
found (default ``False``).
media_type (str): Media type to use for deserialization (e.g.,
``'application/json'``). If ``None``, falls back to the
value of :attr:`~falcon.RequestOptions.default_media_type`
(default ``'application/json'``).
required (bool): Set to ``True`` to raise
:class:`~falcon.HTTPBadRequest` instead of returning ``None``
when the parameter is not found (default ``False``).
store (dict): A ``dict``-like object in which to place the
value of the param, but only if the param is found
(default ``None``).
Expand All @@ -2492,37 +2505,46 @@ def get_param_as_media(

Returns:
The deserialized value for the parameter, or ``default`` if the
parameter is missing and ``required`` is ``False``.
parameter is missing and `required` is ``False``.

Raises:
HTTPBadRequest: A required param is missing from the request, or
the value could not be parsed by the selected media handler.
ValueError: No media handler is configured for `media_type`.

.. _content:
https://spec.openapis.org/oas/latest.html#fixed-fields-for-use-with-content
"""

param_value = self.get_param(name, required=required)

if param_value is None:
return default

# Resolve media handler
if media_type is None:
# Fall back to the app's default media type.
media_type = self.options.default_media_type

handler, _, _ = self.options.media_handlers._resolve(
media_type, self.options.default_media_type, raise_not_found=False
)
if handler is None:
# NOTE(mannxo): Substring match is intentional; covers variants
# like 'application/json; charset=utf-8' and is good enough in
# practice until a stricter check is warranted.
if media_type and MEDIA_JSON in media_type:
handler = _DEFAULT_JSON_HANDLER
else:
raise errors.HTTPInternalServerError(
title=f'No media handler exists for "{media_type}"'
)
raise ValueError(
f'No media handler is configured for {media_type!r}. '
'Please ensure the media type is registered in '
'RequestOptions.media_handlers.'
)

return self._deserialize_param_value(
name, param_value, media_type, handler, store
)

def _deserialize_param_value(
self,
name: str,
param_value: str,
media_type: str,
handler: Any,
store: StoreArg,
) -> Any:
try:
# TODO(CaselIT): find a way to avoid encode + BytesIO if handlers
# interface is refactored. Possibly using the WS interface?
Expand All @@ -2531,7 +2553,7 @@ def get_param_as_media(
)
except errors.HTTPBadRequest:
raise errors.HTTPInvalidParam(
f'It could not be deserialized as "{media_type}".', name
f'It could not be deserialized as {media_type!r}.', name
)

if store is not None:
Expand Down
84 changes: 42 additions & 42 deletions falcon/testing/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -600,13 +600,13 @@ def simulate_request(
iterable yielding a series of two-member (*name*, *value*)
iterables. Each pair of items provides the name and value
for the 'Set-Cookie' header.
msgpack(Msgpack serializable): A Msgpack document to serialize as the
body of the request (default: ``None``). If specified,
overrides `body` and sets the Content-Type header to
``'application/msgpack'``, overriding any value specified by
either the `content_type` or `headers` arguments. If msgpack and json
are both specified, the Content-Type header will be set as
``'application/msgpack'``.
msgpack (MessagePack serializable): A MessagePack document to
serialize as the body of the request (default: ``None``). If
specified, overrides `body` and sets the Content-Type header
to :attr:`~falcon.MEDIA_MSGPACK`, overriding any value
specified by either the `content_type` or `headers`
arguments. If both `msgpack` and `json` are specified,
`msgpack` takes precedence.

Returns:
:class:`~.Result`: The result of the request
Expand Down Expand Up @@ -868,13 +868,13 @@ async def _simulate_request_asgi(
iterable yielding a series of two-member (*name*, *value*)
iterables. Each pair of items provides the name and value
for the 'Set-Cookie' header.
msgpack(Msgpack serializable): A Msgpack document to serialize as the
body of the request (default: ``None``). If specified,
overrides `body` and sets the Content-Type header to
``'application/msgpack'``, overriding any value specified by
either the `content_type` or `headers` arguments. If msgpack and json
are both specified, the Content-Type header will be set as `
`'application/msgpack'``.
msgpack (MessagePack serializable): A MessagePack document to
serialize as the body of the request (default: ``None``). If
specified, overrides `body` and sets the Content-Type header
to :attr:`~falcon.MEDIA_MSGPACK`, overriding any value
specified by either the `content_type` or `headers`
arguments. If both `msgpack` and `json` are specified,
`msgpack` takes precedence.

Returns:
:class:`~.Result`: The result of the request
Expand Down Expand Up @@ -1632,13 +1632,13 @@ def simulate_post(app: Callable[..., Any], path: str, **kwargs: Any) -> Result:
iterable yielding a series of two-member (*name*, *value*)
iterables. Each pair of items provides the name and value
for the 'Set-Cookie' header.
msgpack(Msgpack serializable): A Msgpack document to serialize as the
body of the request (default: ``None``). If specified,
overrides `body` and sets the Content-Type header to
``'application/msgpack'``, overriding any value specified by
either the `content_type` or `headers` arguments. If msgpack and json
are both specified, the Content-Type header will be set as
``'application/msgpack'``.
msgpack (MessagePack serializable): A MessagePack document to
serialize as the body of the request (default: ``None``). If
specified, overrides `body` and sets the Content-Type header
to :attr:`~falcon.MEDIA_MSGPACK`, overriding any value
specified by either the `content_type` or `headers`
arguments. If both `msgpack` and `json` are specified,
`msgpack` takes precedence.

Returns:
:class:`~.Result`: The result of the request
Expand Down Expand Up @@ -1750,13 +1750,13 @@ def simulate_put(app: Callable[..., Any], path: str, **kwargs: Any) -> Result:
iterable yielding a series of two-member (*name*, *value*)
iterables. Each pair of items provides the name and value
for the 'Set-Cookie' header.
msgpack(Msgpack serializable): A Msgpack document to serialize as the
body of the request (default: ``None``). If specified,
overrides `body` and sets the Content-Type header to
``'application/msgpack'``, overriding any value specified by
either the `content_type` or `headers` arguments. If msgpack and json
are both specified, the Content-Type header will be set as
``'application/msgpack'``.
msgpack (MessagePack serializable): A MessagePack document to
serialize as the body of the request (default: ``None``). If
specified, overrides `body` and sets the Content-Type header
to :attr:`~falcon.MEDIA_MSGPACK`, overriding any value
specified by either the `content_type` or `headers`
arguments. If both `msgpack` and `json` are specified,
`msgpack` takes precedence.

Returns:
:class:`~.Result`: The result of the request
Expand Down Expand Up @@ -1952,13 +1952,13 @@ def simulate_patch(app: Callable[..., Any], path: str, **kwargs: Any) -> Result:
iterable yielding a series of two-member (*name*, *value*)
iterables. Each pair of items provides the name and value
for the 'Set-Cookie' header.
msgpack(Msgpack serializable): A Msgpack document to serialize as the
body of the request (default: ``None``). If specified,
overrides `body` and sets the Content-Type header to
``'application/msgpack'``, overriding any value specified by
either the `content_type` or `headers` arguments. If msgpack and json
are both specified, the Content-Type header will be set as
``'application/msgpack'``.
msgpack (MessagePack serializable): A MessagePack document to
serialize as the body of the request (default: ``None``). If
specified, overrides `body` and sets the Content-Type header
to :attr:`~falcon.MEDIA_MSGPACK`, overriding any value
specified by either the `content_type` or `headers`
arguments. If both `msgpack` and `json` are specified,
`msgpack` takes precedence.

Returns:
:class:`~.Result`: The result of the request
Expand Down Expand Up @@ -2065,13 +2065,13 @@ def simulate_delete(app: Callable[..., Any], path: str, **kwargs: Any) -> Result
iterable yielding a series of two-member (*name*, *value*)
iterables. Each pair of items provides the name and value
for the 'Set-Cookie' header.
msgpack(Msgpack serializable): A Msgpack document to serialize as the
body of the request (default: ``None``). If specified,
overrides `body` and sets the Content-Type header to
``'application/msgpack'``, overriding any value specified by
either the `content_type` or `headers` arguments. If msgpack and json
are both specified, the Content-Type header will be set as
``'application/msgpack'``.
msgpack (MessagePack serializable): A MessagePack document to
serialize as the body of the request (default: ``None``). If
specified, overrides `body` and sets the Content-Type header
to :attr:`~falcon.MEDIA_MSGPACK`, overriding any value
specified by either the `content_type` or `headers`
arguments. If both `msgpack` and `json` are specified,
`msgpack` takes precedence.

Returns:
:class:`~.Result`: The result of the request
Expand Down
Loading
Loading