Skip to content

Commit 18eeda9

Browse files
committed
Enable brotli decompression if it is available
1 parent 19bd4e0 commit 18eeda9

File tree

2 files changed

+43
-5
lines changed

2 files changed

+43
-5
lines changed

tests/integration/test_filter.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import pytest
88

99
import vcr
10+
from vcr.filters import brotli
1011

1112
from ..assertions import assert_cassette_has_one_response, assert_is_json_bytes
1213

@@ -138,6 +139,22 @@ def test_decompress_deflate(tmpdir, httpbin):
138139
assert_is_json_bytes(decoded_response)
139140

140141

142+
def test_decompress_brotli(tmpdir, httpbin):
143+
if brotli is None:
144+
# XXX: this is never true, because brotlipy is installed with "httpbin"
145+
pytest.skip("Brotli is not installed")
146+
147+
url = httpbin.url + "/brotli"
148+
request = Request(url, headers={"Accept-Encoding": ["gzip, deflate, br"]})
149+
cass_file = str(tmpdir.join("brotli_response.yaml"))
150+
with vcr.use_cassette(cass_file, decode_compressed_response=True):
151+
urlopen(request)
152+
with vcr.use_cassette(cass_file) as cass:
153+
decoded_response = urlopen(url).read()
154+
assert_cassette_has_one_response(cass)
155+
assert_is_json_bytes(decoded_response)
156+
157+
141158
def test_decompress_regular(tmpdir, httpbin):
142159
"""Test that it doesn't try to decompress content that isn't compressed"""
143160
url = httpbin.url + "/get"

vcr/filters.py

Lines changed: 26 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,15 +150,16 @@ 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.
@@ -157,17 +172,23 @@ def decompress_body(body, encoding):
157172
return zlib.decompress(body, zlib.MAX_WBITS | 16)
158173
except zlib.error:
159174
return body # assumes that the data was already decompressed
160-
else: # encoding == 'deflate'
175+
elif encoding == 'deflate':
161176
try:
162177
return zlib.decompress(body)
163178
except zlib.error:
164179
return body # assumes that the data was already decompressed
180+
else: # encoding == 'br'
181+
try:
182+
return brotli.decompress(body)
183+
except brotli.error:
184+
return body # assumes that the data was already decompressed
185+
165186

166187
# Deepcopy here in case `headers` contain objects that could
167188
# be mutated by a shallow copy and corrupt the real response.
168189
response = copy.deepcopy(response)
169190
headers = CaseInsensitiveDict(response["headers"])
170-
if is_compressed(headers):
191+
if is_decompressable(headers):
171192
encoding = headers["content-encoding"][0]
172193
headers["content-encoding"].remove(encoding)
173194
if not headers["content-encoding"]:

0 commit comments

Comments
 (0)