Skip to content

Commit

Permalink
Add client Brotli support (#5227) (#7100)
Browse files Browse the repository at this point in the history
(cherry picked from commit 28ea32d)

backport ref
#5227 (comment)

Co-authored-by: Dmitry Erlikh <derlih@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
3 people committed Nov 24, 2022
1 parent 640bb1e commit 3e7448a
Show file tree
Hide file tree
Showing 7 changed files with 62 additions and 33 deletions.
1 change: 1 addition & 0 deletions CHANGES/5219.feature
@@ -0,0 +1 @@
Add client brotli compression support (optional with runtime check)
7 changes: 6 additions & 1 deletion aiohttp/client_reqrep.py
Expand Up @@ -49,6 +49,7 @@
set_result,
)
from .http import SERVER_SOFTWARE, HttpVersion10, HttpVersion11, StreamWriter
from .http_parser import HAS_BROTLI
from .log import client_logger
from .streams import StreamReader
from .typedefs import (
Expand Down Expand Up @@ -84,6 +85,10 @@
json_re = re.compile(r"^application/(?:[\w.+-]+?\+)?json")


def _gen_default_accept_encoding() -> str:
return "gzip, deflate, br" if HAS_BROTLI else "gzip, deflate"


@attr.s(auto_attribs=True, frozen=True, slots=True)
class ContentDisposition:
type: Optional[str]
Expand Down Expand Up @@ -229,7 +234,7 @@ class ClientRequest:

DEFAULT_HEADERS = {
hdrs.ACCEPT: "*/*",
hdrs.ACCEPT_ENCODING: "gzip, deflate",
hdrs.ACCEPT_ENCODING: _gen_default_accept_encoding(),
}

body = b""
Expand Down
13 changes: 13 additions & 0 deletions docs/glossary.rst
Expand Up @@ -27,6 +27,19 @@

https://pypi.python.org/pypi/asyncio/

Brotli

Brotli is a generic-purpose lossless compression algorithm that
compresses data using a combination of a modern variant
of the LZ77 algorithm, Huffman coding and second order context modeling,
with a compression ratio comparable to the best currently available
general-purpose compression methods. It is similar in speed with deflate
but offers more dense compression.

The specification of the Brotli Compressed Data Format is defined :rfc:`7932`

https://pypi.org/project/Brotli/

callable

Any object that can be called. Use :func:`callable` to check
Expand Down
6 changes: 6 additions & 0 deletions docs/index.rst
Expand Up @@ -170,6 +170,12 @@ Dependencies
$ pip install aiodns
- *Optional* :term:`Brotli` for brotli (:rfc:`7932`) client compression support.

.. code-block:: bash
$ pip install Brotli
Communication channels
======================
Expand Down
1 change: 1 addition & 0 deletions docs/spelling_wordlist.txt
Expand Up @@ -38,6 +38,7 @@ BodyPartReader
boolean
botocore
brotli
Brotli
brotlipy
bugfix
Bugfixes
Expand Down
15 changes: 14 additions & 1 deletion tests/test_client_request.py
Expand Up @@ -17,6 +17,7 @@
ClientRequest,
ClientResponse,
Fingerprint,
_gen_default_accept_encoding,
_merge_ssl_params,
)
from aiohttp.helpers import PY_310
Expand Down Expand Up @@ -319,7 +320,7 @@ def test_headers(make_request) -> None:

assert "CONTENT-TYPE" in req.headers
assert req.headers["CONTENT-TYPE"] == "text/plain"
assert req.headers["ACCEPT-ENCODING"] == "gzip, deflate"
assert req.headers["ACCEPT-ENCODING"] == "gzip, deflate, br"


def test_headers_list(make_request) -> None:
Expand Down Expand Up @@ -1287,3 +1288,15 @@ def test_loose_cookies_types(loop) -> None:
req.update_cookies(cookies=loose_cookies_type)

loop.run_until_complete(req.close())


@pytest.mark.parametrize(
"has_brotli,expected",
[
(False, "gzip, deflate"),
(True, "gzip, deflate, br"),
],
)
def test_gen_default_accept_encoding(has_brotli, expected) -> None:
with mock.patch("aiohttp.client_reqrep.HAS_BROTLI", has_brotli):
assert _gen_default_accept_encoding() == expected
52 changes: 21 additions & 31 deletions tests/test_web_functional.py
Expand Up @@ -7,6 +7,7 @@
from typing import Optional
from unittest import mock

import brotli
import pytest
from multidict import CIMultiDictProxy, MultiDict
from yarl import URL
Expand Down Expand Up @@ -1078,11 +1079,22 @@ async def handler(request):
await resp.release()


async def test_response_with_precompressed_body_gzip(aiohttp_client) -> None:
@pytest.mark.parametrize(
"compressor,encoding",
[
(zlib.compressobj(wbits=16 + zlib.MAX_WBITS), "gzip"),
(zlib.compressobj(wbits=zlib.MAX_WBITS), "deflate"),
# Actually, wrong compression format, but
# should be supported for some legacy cases.
(zlib.compressobj(wbits=-zlib.MAX_WBITS), "deflate"),
],
)
async def test_response_with_precompressed_body(
aiohttp_client, compressor, encoding
) -> None:
async def handler(request):
headers = {"Content-Encoding": "gzip"}
zcomp = zlib.compressobj(wbits=16 + zlib.MAX_WBITS)
data = zcomp.compress(b"mydata") + zcomp.flush()
headers = {"Content-Encoding": encoding}
data = compressor.compress(b"mydata") + compressor.flush()
return web.Response(body=data, headers=headers)

app = web.Application()
Expand All @@ -1093,17 +1105,15 @@ async def handler(request):
assert 200 == resp.status
data = await resp.read()
assert b"mydata" == data
assert resp.headers.get("Content-Encoding") == "gzip"
assert resp.headers.get("Content-Encoding") == encoding

await resp.release()


async def test_response_with_precompressed_body_deflate(aiohttp_client) -> None:
async def test_response_with_precompressed_body_brotli(aiohttp_client) -> None:
async def handler(request):
headers = {"Content-Encoding": "deflate"}
zcomp = zlib.compressobj(wbits=zlib.MAX_WBITS)
data = zcomp.compress(b"mydata") + zcomp.flush()
return web.Response(body=data, headers=headers)
headers = {"Content-Encoding": "br"}
return web.Response(body=brotli.compress(b"mydata"), headers=headers)

app = web.Application()
app.router.add_get("/", handler)
Expand All @@ -1113,31 +1123,11 @@ async def handler(request):
assert 200 == resp.status
data = await resp.read()
assert b"mydata" == data
assert resp.headers.get("Content-Encoding") == "deflate"
assert resp.headers.get("Content-Encoding") == "br"

await resp.release()


async def test_response_with_precompressed_body_deflate_no_hdrs(aiohttp_client) -> None:
async def handler(request):
headers = {"Content-Encoding": "deflate"}
# Actually, wrong compression format, but
# should be supported for some legacy cases.
zcomp = zlib.compressobj(wbits=-zlib.MAX_WBITS)
data = zcomp.compress(b"mydata") + zcomp.flush()
return web.Response(body=data, headers=headers)

app = web.Application()
app.router.add_get("/", handler)
client = await aiohttp_client(app)

resp = await client.get("/")
assert 200 == resp.status
data = await resp.read()
assert b"mydata" == data
assert resp.headers.get("Content-Encoding") == "deflate"


async def test_bad_request_payload(aiohttp_client) -> None:
async def handler(request):
assert request.method == "POST"
Expand Down

0 comments on commit 3e7448a

Please sign in to comment.