Skip to content

Commit 36d72ea

Browse files
committed
Enable brotli decompression if it is available
1 parent 6fff3ab commit 36d72ea

File tree

3 files changed

+44
-7
lines changed

3 files changed

+44
-7
lines changed

tests/integration/test_filter.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from urllib.parse import urlencode
55
from urllib.error import HTTPError
66
import vcr
7+
from vcr.filters import brotli
78
import json
89
from assertions import assert_cassette_has_one_response, assert_is_json
910

@@ -118,6 +119,22 @@ def test_decompress_deflate(tmpdir, httpbin):
118119
assert_is_json(decoded_response)
119120

120121

122+
def test_decompress_brotli(tmpdir, httpbin):
123+
if brotli is None:
124+
# XXX: this is never true, because brotlipy is installed with "httpbin"
125+
pytest.skip("Brotli is not installed")
126+
127+
url = httpbin.url + "/brotli"
128+
request = Request(url, headers={"Accept-Encoding": ["gzip, deflate, br"]})
129+
cass_file = str(tmpdir.join("brotli_response.yaml"))
130+
with vcr.use_cassette(cass_file, decode_compressed_response=True):
131+
urlopen(request)
132+
with vcr.use_cassette(cass_file) as cass:
133+
decoded_response = urlopen(url).read()
134+
assert_cassette_has_one_response(cass)
135+
assert_is_json(decoded_response)
136+
137+
121138
def test_decompress_regular(tmpdir, httpbin):
122139
"""Test that it doesn't try to decompress content that isn't compressed"""
123140
url = httpbin.url + "/get"

tox.ini

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ skip_missing_interpreters=true
33
envlist =
44
cov-clean,
55
lint,
6-
{py37,py38,py39,py310}-{requests,httplib2,urllib3,tornado4,boto3,aiohttp,httpx},
7-
{pypy3}-{requests,httplib2,urllib3,tornado4,boto3},
6+
{py37,py38,py39,py310}-{requests,httplib2,urllib3,tornado4,boto3,aiohttp,httpx,brotli,brotlipy,brotlicffi},
7+
{pypy3}-{requests,httplib2,urllib3,tornado4,boto3,brotli,brotlipy,brotlicffi},
88
{py310}-httpx019,
99
cov-report
1010

@@ -93,6 +93,9 @@ deps =
9393
# httpx==0.19 is the latest version that supports allow_redirects, newer versions use follow_redirects
9494
httpx019: httpx==0.19
9595
{py37,py38,py39,py310}-{httpx}: pytest-asyncio
96+
brotli: brotli
97+
brotlipy: brotlipy
98+
brotlicffi: brotlicffi
9699
depends =
97100
lint,{py37,py38,py39,py310,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py37,py38,py39,py310}-{aiohttp},{py37,py38,py39,py310}-{httpx}: cov-clean
98101
cov-report: lint,{py37,py38,py39,py310,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py37,py38,py39,py310}-{aiohttp}

vcr/filters.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@
66

77
from .util import CaseInsensitiveDict
88

9+
try:
10+
# This supports both brotli & brotlipy packages
11+
import brotli
12+
except ImportError:
13+
try:
14+
import brotlicffi as brotli
15+
except ImportError:
16+
brotli = None
17+
18+
19+
AVAILABLE_DECOMPRESSORS = {"gzip", "deflate"}
20+
if brotli is not None:
21+
AVAILABLE_DECOMPRESSORS.add("br")
22+
923

1024
def replace_headers(request, replacements):
1125
"""Replace headers in request according to replacements.
@@ -136,30 +150,33 @@ def remove_post_data_parameters(request, post_data_parameters_to_remove):
136150

137151
def decode_response(response):
138152
"""
139-
If the response is compressed with gzip or deflate:
153+
If the response is compressed with any supported compression (gzip,
154+
deflate, br if available):
140155
1. decompress the response body
141156
2. delete the content-encoding header
142157
3. update content-length header to decompressed length
143158
"""
144159

145-
def is_compressed(headers):
160+
def is_decompressable(headers):
146161
encoding = headers.get("content-encoding", [])
147-
return encoding and encoding[0] in ("gzip", "deflate")
162+
return encoding and encoding[0] in AVAILABLE_DECOMPRESSORS
148163

149164
def decompress_body(body, encoding):
150165
"""Returns decompressed body according to encoding using zlib.
151166
to (de-)compress gzip format, use wbits = zlib.MAX_WBITS | 16
152167
"""
153168
if encoding == "gzip":
154169
return zlib.decompress(body, zlib.MAX_WBITS | 16)
155-
else: # encoding == 'deflate'
170+
elif encoding == "deflate":
156171
return zlib.decompress(body)
172+
else: # encoding == 'br'
173+
return brotli.decompress(body)
157174

158175
# Deepcopy here in case `headers` contain objects that could
159176
# be mutated by a shallow copy and corrupt the real response.
160177
response = copy.deepcopy(response)
161178
headers = CaseInsensitiveDict(response["headers"])
162-
if is_compressed(headers):
179+
if is_decompressable(headers):
163180
encoding = headers["content-encoding"][0]
164181
headers["content-encoding"].remove(encoding)
165182
if not headers["content-encoding"]:

0 commit comments

Comments
 (0)