Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .github/workflows/test-suite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ jobs:

strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]

steps:
- uses: "actions/checkout@v4"
- uses: "actions/setup-python@v5"
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with:
python-version: "${{ matrix.python-version }}"
allow-prereleases: true
Expand All @@ -30,5 +30,6 @@ jobs:
run: "scripts/build"
- name: "Run tests"
run: "scripts/test"
timeout-minutes: 10 # TODO(@cclauss): Remove once Python 3.14 tests are passing.
- name: "Enforce coverage"
run: "scripts/coverage"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]`)*
* `backports.zstd` - Decoding for "zstd" compressed responses. *(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
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]`)*
* `backports.zstd` - Decoding for "zstd" compressed responses. *(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
Expand Down
2 changes: 1 addition & 1 deletion docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ b'<!doctype html>\n<html>\n<head>\n<title>Example Domain</title>...'

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`
encoding will be supported. If `backports.zstd` is installed on Python < 3.14, then `zstd`
response encodings will also be supported.

For example, to create an image from binary data returned by a request, you can use the following code:
Expand Down
29 changes: 17 additions & 12 deletions httpx/_decoders.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@
except ImportError:
brotli = None


# Zstandard support is optional
# Zstandard support is optional on Python < 3.14
try:
import zstandard
except ImportError: # pragma: no cover
zstandard = None # type: ignore
from compression import zstd
except ImportError:
try:
from backports import zstd
except ImportError:
zstd = None


class ContentDecoder:
Expand Down Expand Up @@ -162,32 +164,35 @@ class ZStandardDecoder(ContentDecoder):
"""
Handle 'zstd' RFC 8878 decoding.

Requires `pip install zstandard`.
Requires `pip install backports.zstd` on Python < 3.14.
Can be installed as a dependency of httpx using `pip install httpx[zstd]`.
"""

# inspired by the ZstdDecoder implementation in urllib3
def __init__(self) -> None:
if zstandard is None: # pragma: no cover
if zstd is None: # pragma: no cover
raise ImportError(
"Using 'ZStandardDecoder', ..."
"Make sure to install httpx using `pip install httpx[zstd]`."
) from None

self.decompressor = zstandard.ZstdDecompressor().decompressobj()
self.decompressor = zstd
self.decompressor.eof = None
self.decompressor.flush = lambda: None
self.decompressor.unused_data = None
self.seen_data = False

def decode(self, data: bytes) -> bytes:
assert zstandard is not None
assert zstd is not None
self.seen_data = True
output = io.BytesIO()
try:
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.decompressor = zstd
output.write(self.decompressor.decompress(unused_data))
except zstandard.ZstdError as exc:
except zstd.ZstdError as exc:
raise DecodingError(str(exc)) from exc
return output.getvalue()

Expand Down Expand Up @@ -389,5 +394,5 @@ def flush(self) -> list[str]:

if brotli is None:
SUPPORTED_DECODERS.pop("br") # pragma: no cover
if zstandard is None:
if zstd is None:
SUPPORTED_DECODERS.pop("zstd") # pragma: no cover
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ socks = [
"socksio==1.*",
]
zstd = [
"zstandard>=0.18.0",
"backports.zstd>=0.5.0; python_version < '3.14'",
]

[project.scripts]
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ twine==6.1.0
coverage[toml]==7.10.6
cryptography==45.0.7
mypy==1.17.1
pytest==8.4.1
pytest==8.4.2
ruff==0.12.11
trio==0.30.0
trio-typing==0.10.0
Expand Down
6 changes: 5 additions & 1 deletion tests/test_decoders.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@
import typing
import zlib

try:
from compression import zstd
except ImportError:
from backports import zstd

import chardet
import pytest
import zstandard as zstd

import httpx

Expand Down
Loading