Skip to content

Commit aaaa85a

Browse files
committed
gh-43521: Decode gzip responses with gzip.GzipFile
Replace the hand-written _GzipReader with gzip.GzipFile, as requested in review. Unlike _GzipReader, GzipFile bounds how much a sized read decompresses. GzipFile does not close the file object it wraps, so addclosehook closes the response. gzip is imported inside the handler, so importing urllib.request does not import it. Junk after the last gzip member now raises BadGzipFile, as it does with gzip.open(), so the test that expected it to be ignored is removed.
1 parent 3ec8f2c commit aaaa85a

2 files changed

Lines changed: 7 additions & 83 deletions

File tree

‎Lib/test/test_urllib2.py‎

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2123,15 +2123,6 @@ def test_multiple_gzip_members_decoded(self):
21232123
with h.http_response(req, response) as result:
21242124
self.assertEqual(result.read(), b"AAABBB")
21252125

2126-
def test_trailing_junk_after_member_ignored(self):
2127-
# Bytes after the final member that are not another gzip member are
2128-
# tolerated: the body decodes without error and is not corrupted.
2129-
h, req = self.opened_request()
2130-
comp = self.gzip_bytes(self.body) + b"trailing garbage"
2131-
response = self.make_response(comp)
2132-
with h.http_response(req, response) as result:
2133-
self.assertEqual(result.read(), self.body)
2134-
21352126
def test_user_accept_encoding_passes_through(self):
21362127
# A caller-supplied Accept-Encoding is left to the caller to decode:
21372128
# the gzipped response is returned untouched.
@@ -2164,7 +2155,7 @@ def test_close_propagates(self):
21642155
self.assertTrue(response.closed)
21652156

21662157
def test_requires_zlib(self):
2167-
with mock.patch.object(urllib.request, "zlib", None):
2158+
with mock.patch.dict(sys.modules, {"gzip": None}):
21682159
with self.assertRaises(ImportError):
21692160
urllib.request.HTTPGzipHandler()
21702161

‎Lib/urllib/request.py‎

Lines changed: 6 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,6 @@
114114
else:
115115
_have_ssl = True
116116

117-
try:
118-
import zlib
119-
except ImportError:
120-
zlib = None
121-
122117
__all__ = [
123118
# Classes
124119
'Request', 'OpenerDirector', 'BaseHandler', 'HTTPDefaultErrorHandler',
@@ -1418,70 +1413,6 @@ def http_response(self, request, response):
14181413
https_response = http_response
14191414

14201415

1421-
class _GzipReader(io.BufferedIOBase):
1422-
"""Incrementally decode a gzip Content-Encoding response body."""
1423-
1424-
def __init__(self, fp):
1425-
self._fp = fp
1426-
self._decompressor = zlib.decompressobj(16 + zlib.MAX_WBITS)
1427-
self._buffer = b''
1428-
self._leftover = b''
1429-
self._got_data = False
1430-
self._eof = False
1431-
1432-
def readable(self):
1433-
return True
1434-
1435-
def _refill(self):
1436-
while True:
1437-
raw = self._leftover or self._fp.read(io.DEFAULT_BUFFER_SIZE)
1438-
self._leftover = b''
1439-
if not raw:
1440-
self._eof = True
1441-
data = self._decompressor.flush()
1442-
if self._got_data and not self._decompressor.eof:
1443-
raise EOFError('compressed response ended before the '
1444-
'end-of-stream marker was reached')
1445-
return data
1446-
self._got_data = True
1447-
if self._decompressor.eof:
1448-
# The previous member ended; the remaining bytes start the
1449-
# next concatenated member. Decode it with a fresh
1450-
# decompressor, and tolerate trailing bytes that are not
1451-
# another member, as the reader did before.
1452-
self._decompressor = zlib.decompressobj(16 + zlib.MAX_WBITS)
1453-
try:
1454-
data = self._decompressor.decompress(raw)
1455-
except zlib.error:
1456-
self._eof = True
1457-
return b''
1458-
else:
1459-
data = self._decompressor.decompress(raw)
1460-
self._leftover = self._decompressor.unused_data
1461-
if data:
1462-
return data
1463-
1464-
def read(self, size=-1):
1465-
if size is None:
1466-
size = -1
1467-
while not self._eof and (size < 0 or len(self._buffer) < size):
1468-
self._buffer += self._refill()
1469-
if size < 0:
1470-
data, self._buffer = self._buffer, b''
1471-
else:
1472-
data, self._buffer = self._buffer[:size], self._buffer[size:]
1473-
return data
1474-
1475-
def close(self):
1476-
try:
1477-
fp = self._fp
1478-
if fp is not None:
1479-
self._fp = None
1480-
fp.close()
1481-
finally:
1482-
super().close()
1483-
1484-
14851416
class HTTPGzipHandler(BaseHandler):
14861417
"""Request and transparently decode gzip Content-Encoding responses.
14871418
@@ -1493,8 +1424,8 @@ class HTTPGzipHandler(BaseHandler):
14931424
"""
14941425

14951426
def __init__(self):
1496-
if zlib is None:
1497-
raise ImportError('the zlib module is required for gzip decoding')
1427+
# Raise ImportError now, not on the first response, if zlib is missing.
1428+
import gzip # noqa: F401
14981429

14991430
def http_request(self, request):
15001431
if not request.has_header('Accept-encoding'):
@@ -1505,11 +1436,13 @@ def http_request(self, request):
15051436
def http_response(self, request, response):
15061437
if (getattr(request, '_gzip_injected', False) and
15071438
response.headers.get('Content-Encoding', '').lower() == 'gzip'):
1439+
import gzip
15081440
headers = copy.copy(response.headers)
15091441
del headers['Content-Encoding']
15101442
del headers['Content-Length']
1511-
result = addinfourl(_GzipReader(response), headers,
1512-
response.url, response.code)
1443+
fp = addclosehook(gzip.GzipFile(fileobj=response, mode='rb'),
1444+
response.close)
1445+
result = addinfourl(fp, headers, response.url, response.code)
15131446
result.msg = response.msg
15141447
return result
15151448
return response

0 commit comments

Comments
 (0)