diff --git a/docs/_newsfragments/2110.newandimproved.rst b/docs/_newsfragments/2110.newandimproved.rst new file mode 100644 index 000000000..a8cc0f8da --- /dev/null +++ b/docs/_newsfragments/2110.newandimproved.rst @@ -0,0 +1,6 @@ +Following the recommendation from +`RFC 9239 `__, the +:ref:`MEDIA_JS ` constant has been updated to +``text/javascript``. Furthermore, this and other media type constants are now +preferred to the stdlib's :mod:`mimetypes` for the initialization of +:attr:`~falcon.ResponseOptions.static_media_types`. diff --git a/falcon/constants.py b/falcon/constants.py index dbf3433fb..9bd71f373 100644 --- a/falcon/constants.py +++ b/falcon/constants.py @@ -105,14 +105,10 @@ # contrary to the RFCs. MEDIA_XML = 'application/xml' -# NOTE(kgriffs): RFC 4329 recommends application/* over text/. -# furthermore, parsers are required to respect the Unicode -# encoding signature, if present in the document, and to default -# to UTF-8 when not present. Note, however, that implementations -# are not required to support anything besides UTF-8, so it is -# unclear how much utility an encoding signature (or the charset -# parameter for that matter) has in practice. -MEDIA_JS = 'application/javascript' +# NOTE(euj1n0ng): According to RFC 9239, Changed the intended usage of the +# media type "text/javascript" from OBSOLETE to COMMON. Changed +# the intended usage for all other script media types to obsolete. +MEDIA_JS = 'text/javascript' # NOTE(kgriffs): According to RFC 6838, most text media types should # include the charset parameter. @@ -141,6 +137,30 @@ ] ) +# NOTE(vytas): We strip the preferred charsets from the default static file +# type mapping as it is hard to make any assumptions without knowing which +# files are going to be served. Moreover, the popular web servers (like +# Nginx) do not try to guess either. +_DEFAULT_STATIC_MEDIA_TYPES = tuple( + (ext, media_type.split(';', 1)[0]) + for ext, media_type in ( + ('.bmp', MEDIA_BMP), + ('.gif', MEDIA_GIF), + ('.htm', MEDIA_HTML), + ('.html', MEDIA_HTML), + ('.jpeg', MEDIA_JPEG), + ('.jpg', MEDIA_JPEG), + ('.js', MEDIA_JS), + ('.json', MEDIA_JSON), + ('.mjs', MEDIA_JS), + ('.png', MEDIA_PNG), + ('.txt', MEDIA_TEXT), + ('.xml', MEDIA_XML), + ('.yaml', MEDIA_YAML), + ('.yml', MEDIA_YAML), + ) +) + # NOTE(kgriffs): Special singleton to be used internally whenever using # None would be ambiguous. _UNSET = object() diff --git a/falcon/response.py b/falcon/response.py index 37941991f..f69ff082f 100644 --- a/falcon/response.py +++ b/falcon/response.py @@ -17,6 +17,7 @@ import functools import mimetypes +from falcon.constants import _DEFAULT_STATIC_MEDIA_TYPES from falcon.constants import _UNSET from falcon.constants import DEFAULT_MEDIA_TYPE from falcon.errors import HeaderNotSupported @@ -1246,4 +1247,5 @@ def __init__(self): if not mimetypes.inited: mimetypes.init() - self.static_media_types = mimetypes.types_map + self.static_media_types = mimetypes.types_map.copy() + self.static_media_types.update(_DEFAULT_STATIC_MEDIA_TYPES) diff --git a/tests/test_response.py b/tests/test_response.py index 697197506..fe2b73d93 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -65,13 +65,20 @@ def test_response_removed_stream_len(resp): def test_response_option_mimetype_init(monkeypatch): mock = MagicMock() mock.inited = False + mock.types_map = {'.js': 'application/javascript'} monkeypatch.setattr('falcon.response.mimetypes', mock) ro = ResponseOptions() - assert ro.static_media_types is mock.types_map + assert ro.static_media_types['.js'] == 'text/javascript' + assert ro.static_media_types['.json'] == 'application/json' + assert ro.static_media_types['.mjs'] == 'text/javascript' + mock.reset_mock() mock.inited = True ro = ResponseOptions() - assert ro.static_media_types is mock.types_map mock.init.assert_not_called() + + assert ro.static_media_types['.js'] == 'text/javascript' + assert ro.static_media_types['.json'] == 'application/json' + assert ro.static_media_types['.mjs'] == 'text/javascript'