Repository: encode/httpx

Issue:
DecodingError with zlib-compressed responses
Using the following minimal WSGI app:

```python
import zlib

def app(environ, start_response):
    start_response("200 OK", [("Content-Encoding", "deflate")])
    return [zlib.compress(b"hello world")]
```

This works fine in a web browser.

Requests is quite happy:

```
>>> import requests
>>> requests.get("http://localhost:9999")
<Response [200]>
>>> requests.get("http://localhost:9999").text
'hello world'
```

httpx says:

```
>>> httpx.get('http://localhost:9999')
Traceback (most recent call last):
  File "/Users/jamie/code/httpx/httpx/decoders.py", line 52, in decode
    return self.decompressor.decompress(data)
zlib.error: Error -3 while decompressing data: invalid stored block lengths

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/jamie/code/httpx/httpx/api.py", line 170, in get
    trust_env=trust_env,
  File "/Users/jamie/code/httpx/httpx/api.py", line 96, in request
    allow_redirects=allow_redirects,
  File "/Users/jamie/code/httpx/httpx/client.py", line 568, in request
    request, auth=auth, allow_redirects=allow_redirects, timeout=timeout,
  File "/Users/jamie/code/httpx/httpx/client.py", line 593, in send
    response.read()
  File "/Users/jamie/code/httpx/httpx/models.py", line 900, in read
    self._content = b"".join([part for part in self.iter_bytes()])
  File "/Users/jamie/code/httpx/httpx/models.py", line 900, in <listcomp>
    self._content = b"".join([part for part in self.iter_bytes()])
  File "/Users/jamie/code/httpx/httpx/models.py", line 912, in iter_bytes
    yield self.decoder.decode(chunk)
  File "/Users/jamie/code/httpx/httpx/decoders.py", line 54, in decode
    raise DecodingError from exc
httpx.exceptions.DecodingError
```

The reason this is an issue and not a PR is because I don't understand the code in `decoders.py` - it very explicitly says `self.decompressor = zlib.decompressobj(-zlib.MAX_WBITS)`, which according to [the Python docs](https://docs.python.org/3/library/zlib.html#zlib.compressobj) means "Uses the absolute value of wbits as the window size logarithm, while producing a raw output stream with no header or trailing checksum." I don't know enough about zlib to understand this, but it looks very deliberate so I thought I'd better ask.


Discussion:

Resolving patch:
diff --git a/httpx/decoders.py b/httpx/decoders.py
--- a/httpx/decoders.py
+++ b/httpx/decoders.py
@@ -45,12 +45,18 @@ class DeflateDecoder(Decoder):
     """
 
     def __init__(self) -> None:
-        self.decompressor = zlib.decompressobj(-zlib.MAX_WBITS)
+        self.first_attempt = True
+        self.decompressor = zlib.decompressobj()
 
     def decode(self, data: bytes) -> bytes:
+        was_first_attempt = self.first_attempt
+        self.first_attempt = False
         try:
             return self.decompressor.decompress(data)
         except zlib.error as exc:
+            if was_first_attempt:
+                self.decompressor = zlib.decompressobj(-zlib.MAX_WBITS)
+                return self.decode(data)
             raise DecodingError from exc
 
     def flush(self) -> bytes:


Test patch:
diff --git a/tests/test_decoders.py b/tests/test_decoders.py
--- a/tests/test_decoders.py
+++ b/tests/test_decoders.py
@@ -18,6 +18,11 @@
 
 
 def test_deflate():
+    """
+    Deflate encoding may use either 'zlib' or 'deflate' in the wild.
+
+    https://stackoverflow.com/questions/1838699/how-can-i-decompress-a-gzip-stream-with-zlib#answer-22311297
+    """
     body = b"test 123"
     compressor = zlib.compressobj(9, zlib.DEFLATED, -zlib.MAX_WBITS)
     compressed_body = compressor.compress(body) + compressor.flush()
@@ -29,6 +34,22 @@ def test_deflate():
     assert response.content == body
 
 
+def test_zlib():
+    """
+    Deflate encoding may use either 'zlib' or 'deflate' in the wild.
+
+    https://stackoverflow.com/questions/1838699/how-can-i-decompress-a-gzip-stream-with-zlib#answer-22311297
+    """
+    body = b"test 123"
+    compressed_body = zlib.compress(body)
+
+    headers = [(b"Content-Encoding", b"deflate")]
+    response = httpx.Response(
+        200, headers=headers, content=compressed_body, request=REQUEST
+    )
+    assert response.content == body
+
+
 def test_gzip():
     body = b"test 123"
     compressor = zlib.compressobj(9, zlib.DEFLATED, zlib.MAX_WBITS | 16)