Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #2206: Allow content-types "application/xxx+json" in ClientResponse.json() #2819

Merged
merged 2 commits into from
Mar 9, 2018
Merged
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
1 change: 1 addition & 0 deletions CHANGES/2206.doc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Change ```ClientResponse.json()``` documentation to reflect that it now allows "application/xxx+json" content-types
2 changes: 2 additions & 0 deletions CHANGES/2206.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Relax JSON content-type checking in the ``ClientResponse.json()`` to allow
"application/xxx+json" instead of strict "application/json".
12 changes: 11 additions & 1 deletion aiohttp/client_reqrep.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import codecs
import io
import json
import re
import sys
import traceback
import warnings
Expand Down Expand Up @@ -39,6 +40,9 @@
__all__ = ('ClientRequest', 'ClientResponse', 'RequestInfo', 'Fingerprint')


json_re = re.compile('^application/(?:[\w.+-]+?\+)?json')


@attr.s(frozen=True, slots=True)
class ContentDisposition:
type = attr.ib(type=str)
Expand Down Expand Up @@ -131,6 +135,12 @@ def _merge_ssl_params(ssl, verify_ssl, ssl_context, fingerprint):
ConnectionKey = namedtuple('ConnectionKey', ['host', 'port', 'ssl'])


def _is_expected_content_type(response_content_type, expected_content_type):
if expected_content_type == 'application/json':
return json_re.match(response_content_type)
return expected_content_type in response_content_type


class ClientRequest:
GET_METHODS = {
hdrs.METH_GET,
Expand Down Expand Up @@ -859,7 +869,7 @@ async def json(self, *, encoding=None, loads=json.loads,

if content_type:
ctype = self.headers.get(hdrs.CONTENT_TYPE, '').lower()
if content_type not in ctype:
if not _is_expected_content_type(ctype, content_type):
raise ContentTypeError(
self.request_info,
self.history,
Expand Down
15 changes: 10 additions & 5 deletions docs/client_advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -215,13 +215,18 @@ Disabling content type validation for JSON responses
----------------------------------------------------

The standard explicitly restricts JSON ``Content-Type`` HTTP header to
``apllication/json``. Unfortunately some servers send wrong type like
``text/html`` or custom one, e.g. ``application/vnd.custom-type+json``.
``application/json`` or any extended form, e.g. ``application/vnd.custom-type+json``.
Unfortunately, some servers send a wrong type, like ``text/html``.

In this case there are two options:
This can be worked around in two ways:

1. Pass expected type explicitly: ``await resp.json(content_type='custom')``.
2. Disable the check entirely: ``await resp.json(content_type=None)``.
1. Pass the expected type explicitly (in this case checking will be strict, without the extended form support,
so ``custom/xxx+type`` won't be accepted):

``await resp.json(content_type='custom/type')``.
2. Disable the check entirely:

``await resp.json(content_type=None)``.

.. _aiohttp-client-tracing:

Expand Down
39 changes: 39 additions & 0 deletions tests/test_client_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,45 @@ def side_effect(*args, **kwargs):
assert response._connection is None


async def test_json_extended_content_type(loop, session):
response = ClientResponse('get', URL('http://def-cl-resp.org'))
response._post_init(loop, session)

def side_effect(*args, **kwargs):
fut = loop.create_future()
fut.set_result('{"тест": "пройден"}'.encode('cp1251'))
return fut

response.headers = {
'Content-Type':
'application/this.is-1_content+subtype+json;charset=cp1251'}
content = response.content = mock.Mock()
content.read.side_effect = side_effect

res = await response.json()
assert res == {'тест': 'пройден'}
assert response._connection is None


async def test_json_custom_content_type(loop, session):
response = ClientResponse('get', URL('http://def-cl-resp.org'))
response._post_init(loop, session)

def side_effect(*args, **kwargs):
fut = loop.create_future()
fut.set_result('{"тест": "пройден"}'.encode('cp1251'))
return fut

response.headers = {
'Content-Type': 'custom/type;charset=cp1251'}
content = response.content = mock.Mock()
content.read.side_effect = side_effect

res = await response.json(content_type='custom/type')
assert res == {'тест': 'пройден'}
assert response._connection is None


async def test_json_custom_loader(loop, session):
response = ClientResponse('get', URL('http://def-cl-resp.org'))
response._post_init(loop, session)
Expand Down