diff --git a/README.md b/README.md
index 2ccecd578c..d3271aad93 100644
--- a/README.md
+++ b/README.md
@@ -136,7 +136,7 @@ As well as these optional installs:
* `rich` - Rich terminal support. *(Optional, with `httpx[cli]`)*
* `click` - Command line client support. *(Optional, with `httpx[cli]`)*
* `brotli` or `brotlicffi` - Decoding for "brotli" compressed responses. *(Optional, with `httpx[brotli]`)*
-* `zstandard` - Decoding for "zstd" compressed responses. *(Optional, with `httpx[zstd]`)*
+* `zstandard` - Decoding for "zstd" compressed responses before Python 3.14. *(Optional, with `httpx[zstd]`)*
A huge amount of credit is due to `requests` for the API layout that
much of this work follows, as well as to `urllib3` for plenty of design
diff --git a/docs/index.md b/docs/index.md
index 90a4f6b6f7..d5546d68a3 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -119,7 +119,7 @@ As well as these optional installs:
* `rich` - Rich terminal support. *(Optional, with `httpx[cli]`)*
* `click` - Command line client support. *(Optional, with `httpx[cli]`)*
* `brotli` or `brotlicffi` - Decoding for "brotli" compressed responses. *(Optional, with `httpx[brotli]`)*
-* `zstandard` - Decoding for "zstd" compressed responses. *(Optional, with `httpx[zstd]`)*
+* `zstandard` - Decoding for "zstd" compressed responses before Python 3.14. *(Optional, with `httpx[zstd]`)*
A huge amount of credit is due to `requests` for the API layout that
much of this work follows, as well as to `urllib3` for plenty of design
diff --git a/docs/quickstart.md b/docs/quickstart.md
index 38da2fec36..a226bf17a2 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -100,8 +100,9 @@ b'\n\n
\nExample Domain...'
Any `gzip` and `deflate` HTTP response encodings will automatically
be decoded for you. If `brotlipy` is installed, then the `brotli` response
-encoding will be supported. If `zstandard` is installed, then `zstd`
-response encodings will also be supported.
+encoding will be supported. The `zstd` response encoding is supported by
+default on Python 3.14 and later, and optionally available on earlier Python
+versions with `zstandard` installed.
For example, to create an image from binary data returned by a request, you can use the following code:
diff --git a/httpx/_decoders.py b/httpx/_decoders.py
index 899dfada87..e5f1556aef 100644
--- a/httpx/_decoders.py
+++ b/httpx/_decoders.py
@@ -26,11 +26,15 @@
brotli = None
-# Zstandard support is optional
+# Zstandard support is avaible in the standard library from 3.14 and later,
+# or by an optional dependency on `zstandard`
try:
- import zstandard
+ import compression.zstd as zstandard
except ImportError: # pragma: no cover
- zstandard = None # type: ignore
+ try:
+ import zstandard
+ except ImportError: # pragma: no cover
+ zstandard = None
class ContentDecoder:
@@ -174,9 +178,16 @@ def __init__(self) -> None:
"Make sure to install httpx using `pip install httpx[zstd]`."
) from None
- self.decompressor = zstandard.ZstdDecompressor().decompressobj()
+ self._new_decompressor()
self.seen_data = False
+ def _new_decompressor(self) -> None:
+ decompressor = zstandard.ZstdDecompressor()
+ if hasattr(decompressor, "decompressobj"):
+ self.decompressor = decompressor.decompressobj() # prgama: no cover
+ else:
+ self.decompressor = decompressor # pragma: no cover
+
def decode(self, data: bytes) -> bytes:
assert zstandard is not None
self.seen_data = True
@@ -185,19 +196,16 @@ def decode(self, data: bytes) -> bytes:
output.write(self.decompressor.decompress(data))
while self.decompressor.eof and self.decompressor.unused_data:
unused_data = self.decompressor.unused_data
- self.decompressor = zstandard.ZstdDecompressor().decompressobj()
+ self._new_decompressor()
output.write(self.decompressor.decompress(unused_data))
except zstandard.ZstdError as exc:
raise DecodingError(str(exc)) from exc
return output.getvalue()
def flush(self) -> bytes:
- if not self.seen_data:
- return b""
- ret = self.decompressor.flush() # note: this is a no-op
- if not self.decompressor.eof:
+ if self.seen_data and not self.decompressor.eof:
raise DecodingError("Zstandard data is incomplete") # pragma: no cover
- return bytes(ret)
+ return b""
class MultiDecoder(ContentDecoder):
diff --git a/pyproject.toml b/pyproject.toml
index eb9e5c9a3d..5445002707 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -52,7 +52,7 @@ socks = [
"socksio==1.*",
]
zstd = [
- "zstandard>=0.18.0",
+ "zstandard>=0.18.0; python_version < \"3.14\"",
]
[project.scripts]
diff --git a/tests/test_decoders.py b/tests/test_decoders.py
index 9ffaba189d..66d70b24d9 100644
--- a/tests/test_decoders.py
+++ b/tests/test_decoders.py
@@ -6,7 +6,11 @@
import chardet
import pytest
-import zstandard as zstd
+
+try:
+ from compression import zstd
+except ImportError:
+ import zstandard as zstd
import httpx