Repository: urllib3/urllib3

Issue:
Content-Encoding with multiple encodings not supported
It is possible for a server to respond with a Content-Encoding header like:
`Content-Encoding: deflate, deflate`

From RFC7231:
> If one or more encodings have been applied to a representation, the
> sender that applied the encodings MUST generate a Content-Encoding
> header field that lists the content codings in the order in which
> they were applied.

This can be tested with:

```python
def test_multiple_content_encodings():
    data = zlib.compress(zlib.compress(b'foo'))
    fp = BytesIO(data)
    r = HTTPResponse(fp, headers={'Content-Encoding': 'deflate, deflate'})
    assert r.data == b'foo'
````

Would it make sense to do something like:
````python
if len(self.headers.get('content-encoding').split(',') > 1:
    self._decoder = MultiDecoder(self.headers.get('content-encoding'))
````
where MultiDecoder just iterates through the list applying standard decoders?


Discussion:
TIL that `Content-Encoding` allows for nested encodings. Do you have an example site that does this? We haven't hit this problem until now.

I'm open to a PR that implements this behavior.

Resolving patch:
diff --git a/src/urllib3/response.py b/src/urllib3/response.py
--- a/src/urllib3/response.py
+++ b/src/urllib3/response.py
@@ -90,7 +90,31 @@ def decompress(self, data):
             self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS)
 
 
+class MultiDecoder(object):
+    """
+    From RFC7231:
+        If one or more encodings have been applied to a representation, the
+        sender that applied the encodings MUST generate a Content-Encoding
+        header field that lists the content codings in the order in which
+        they were applied.
+    """
+
+    def __init__(self, modes):
+        self._decoders = [_get_decoder(m.strip()) for m in modes.split(',')]
+
+    def flush(self):
+        return self._decoders[0].flush()
+
+    def decompress(self, data):
+        for d in reversed(self._decoders):
+            data = d.decompress(data)
+        return data
+
+
 def _get_decoder(mode):
+    if ',' in mode:
+        return MultiDecoder(mode)
+
     if mode == 'gzip':
         return GzipDecoder()
 
@@ -283,8 +307,13 @@ def _init_decoder(self):
         # Note: content-encoding value should be case-insensitive, per RFC 7230
         # Section 3.2
         content_encoding = self.headers.get('content-encoding', '').lower()
-        if self._decoder is None and content_encoding in self.CONTENT_DECODERS:
-            self._decoder = _get_decoder(content_encoding)
+        if self._decoder is None:
+            if content_encoding in self.CONTENT_DECODERS:
+                self._decoder = _get_decoder(content_encoding)
+            elif ',' in content_encoding:
+                encodings = [e.strip() for e in content_encoding.split(',') if e.strip() in self.CONTENT_DECODERS]
+                if len(encodings):
+                    self._decoder = _get_decoder(content_encoding)
 
     def _decode(self, data, decode_content, flush_decoder):
         """


Test patch:
diff --git a/test/test_response.py b/test/test_response.py
--- a/test/test_response.py
+++ b/test/test_response.py
@@ -208,6 +208,38 @@ def test_chunked_decoding_gzip_swallow_garbage(self):
 
         assert r.data == b'foofoofoo'
 
+    def test_multi_decoding_deflate_deflate(self):
+        data = zlib.compress(zlib.compress(b'foo'))
+
+        fp = BytesIO(data)
+        r = HTTPResponse(fp, headers={'content-encoding': 'deflate, deflate'})
+
+        assert r.data == b'foo'
+
+    def test_multi_decoding_deflate_gzip(self):
+        compress = zlib.compressobj(6, zlib.DEFLATED, 16 + zlib.MAX_WBITS)
+        data = compress.compress(zlib.compress(b'foo'))
+        data += compress.flush()
+
+        fp = BytesIO(data)
+        r = HTTPResponse(fp, headers={'content-encoding': 'deflate, gzip'})
+
+        assert r.data == b'foo'
+
+    def test_multi_decoding_gzip_gzip(self):
+        compress = zlib.compressobj(6, zlib.DEFLATED, 16 + zlib.MAX_WBITS)
+        data = compress.compress(b'foo')
+        data += compress.flush()
+
+        compress = zlib.compressobj(6, zlib.DEFLATED, 16 + zlib.MAX_WBITS)
+        data = compress.compress(data)
+        data += compress.flush()
+
+        fp = BytesIO(data)
+        r = HTTPResponse(fp, headers={'content-encoding': 'gzip, gzip'})
+
+        assert r.data == b'foo'
+
     def test_body_blob(self):
         resp = HTTPResponse(b'foo')
         assert resp.data == b'foo'