Skip to content

Commit

Permalink
Fix #2206: Allow content-types "application/xxx+json" in ClientRespon…
Browse files Browse the repository at this point in the history
…se.json() (#2819)

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

* Add test for custom content type; fix a bug in checking
  • Loading branch information
livercat authored and asvetlov committed Mar 9, 2018
1 parent 7f7b12a commit 3debc53
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 6 deletions.
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

0 comments on commit 3debc53

Please sign in to comment.