Repository: urllib3/urllib3

Issue:
Support `Content-Encoding: x-gzip`
### Context

I'm using an API that returns a gzipped response, but the reported `Content-Encoding` is `x-gzip` rather than `gzip`, so urllib3 fails to decompress it.

According to [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html#section-8.4.1.3):

>  A recipient SHOULD consider "x-gzip" to be equivalent to "gzip".

Therefore, I would expect e.g. `Response.read(decode_content=True)` to automatically decompress such responses.

Unfortunately, I can't find a public server that would produce such responses, so I can't provide a simple reproducer.

### Alternatives

I can set `decode_content=False` and gunzip the content manually.

### Duplicate

> Has the feature been requested before?

Not that I could find.

### Contribution

> Would you be willing to submit a PR?

Probably not.




Discussion:
https://github.com/urllib3/urllib3/blob/7d0648b53cf4159e7f3ebb8353cb642942ec93e6/src/urllib3/response.py#L207-L220 could be trivially updated to say, after we get past the MultiDecoder return do a `if mode.startswith("x-"):\n\t\tmode = mode[2:]` and then we don't need to worry about `x-gzip` or `x-compress`. And if any other encodings are eXperimental first, we can handle those backwards compatibly as well.
FWIW, I don't think it's a good idea to blindly assume that `x-foo` is the same as `foo`, since there is no guarantee that will be the case. Plus, the only registered `x-` encodings are `x-gzip` and `x-compress` (and urllib3 doesn't even support `compress`).


Resolving patch:
diff --git a/src/urllib3/response.py b/src/urllib3/response.py
--- a/src/urllib3/response.py
+++ b/src/urllib3/response.py
@@ -208,7 +208,9 @@ def _get_decoder(mode: str) -> ContentDecoder:
     if "," in mode:
         return MultiDecoder(mode)
 
-    if mode == "gzip":
+    # According to RFC 9110 section 8.4.1.3, recipients should
+    # consider x-gzip equivalent to gzip
+    if mode in ("gzip", "x-gzip"):
         return GzipDecoder()
 
     if brotli is not None and mode == "br":
@@ -280,7 +282,7 @@ def get(self, n: int) -> bytes:
 
 
 class BaseHTTPResponse(io.IOBase):
-    CONTENT_DECODERS = ["gzip", "deflate"]
+    CONTENT_DECODERS = ["gzip", "x-gzip", "deflate"]
     if brotli is not None:
         CONTENT_DECODERS += ["br"]
     if zstd is not None:


Test patch:
diff --git a/test/test_response.py b/test/test_response.py
--- a/test/test_response.py
+++ b/test/test_response.py
@@ -229,14 +229,15 @@ def test_chunked_decoding_deflate2(self) -> None:
         assert r.read() == b""
         assert r.read() == b""
 
-    def test_chunked_decoding_gzip(self) -> None:
+    @pytest.mark.parametrize("content_encoding", ["gzip", "x-gzip"])
+    def test_chunked_decoding_gzip(self, content_encoding: str) -> None:
         compress = zlib.compressobj(6, zlib.DEFLATED, 16 + zlib.MAX_WBITS)
         data = compress.compress(b"foo")
         data += compress.flush()
 
         fp = BytesIO(data)
         r = HTTPResponse(
-            fp, headers={"content-encoding": "gzip"}, preload_content=False
+            fp, headers={"content-encoding": content_encoding}, preload_content=False
         )
 
         assert r.read(1) == b"f"