Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: encode/httpx
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 0.24.1
Choose a base ref
...
head repository: encode/httpx
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 0.25.0
Choose a head ref
Loading
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ jobs:
- uses: "actions/checkout@v3"
- uses: "actions/setup-python@v4"
with:
python-version: 3.7
python-version: 3.8
- name: "Install dependencies"
run: "scripts/install"
- name: "Build package & docs"
2 changes: 1 addition & 1 deletion .github/workflows/test-suite.yml
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ jobs:

strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
python-version: ["3.8", "3.9", "3.10", "3.11"]

steps:
- uses: "actions/checkout@v3"
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -4,6 +4,26 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## 0.25.0 (11th Sep, 2023)

### Removed

* Drop support for Python 3.7. (#2813)

### Added

* Support HTTPS proxies. (#2845)
* Change the type of `Extensions` from `Mapping[Str, Any]` to `MutableMapping[Str, Any]`. (#2803)
* Add `socket_options` argument to `httpx.HTTPTransport` and `httpx.AsyncHTTPTransport` classes. (#2716)
* The `Response.raise_for_status()` method now returns the response instance. For example: `data = httpx.get('...').raise_for_status().json()`. (#2776)

### Fixed

* Return `500` error response instead of exceptions when `raise_app_exceptions=False` is set on `ASGITransport`. (#2669)
* Ensure all `WSGITransport` environs have a `SERVER_PROTOCOL`. (#2708)
* Always encode forward slashes as `%2F` in query parameters (#2723)
* Use Mozilla documentation instead of `httpstatuses.com` for HTTP error reference (#2768)

## 0.24.1 (17th May, 2023)

### Added
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -103,7 +103,7 @@ Or, to include the optional HTTP/2 support, use:
$ pip install httpx[http2]
```

HTTPX requires Python 3.7+.
HTTPX requires Python 3.8+.

## Documentation

2 changes: 1 addition & 1 deletion README_chinese.md
Original file line number Diff line number Diff line change
@@ -101,7 +101,7 @@ $ pip install httpx
$ pip install httpx[http2]
```

HTTPX 要求 Python 3.7+ 版本。
HTTPX 要求 Python 3.8+ 版本。

## 文档

32 changes: 32 additions & 0 deletions docs/advanced.md
Original file line number Diff line number Diff line change
@@ -426,6 +426,38 @@ with tempfile.NamedTemporaryFile() as download_file:

![rich progress bar](img/rich-progress.gif)

## Monitoring upload progress

If you need to monitor upload progress of large responses, you can use request content generator streaming.

For example, showing a progress bar using the [`tqdm`](https://github.com/tqdm/tqdm) library.

```python
import io
import random

import httpx
from tqdm import tqdm


def gen():
"""
this is a complete example with generated random bytes.
you can replace `io.BytesIO` with real file object.
"""
total = 32 * 1024 * 1024 # 32m
with tqdm(ascii=True, unit_scale=True, unit='B', unit_divisor=1024, total=total) as bar:
with io.BytesIO(random.randbytes(total)) as f:
while data := f.read(1024):
yield data
bar.update(len(data))


httpx.post("https://httpbin.org/post", content=gen())
```

![tqdm progress bar](img/tqdm-progress.gif)

## .netrc Support

HTTPX can be configured to use [a `.netrc` config file](https://everything.curl.dev/usingcurl/netrc) for authentication.
2 changes: 1 addition & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
@@ -70,7 +70,7 @@
* The amount of time elapsed between sending the request and calling `close()` on the corresponding response received for that request.
[total_seconds()](https://docs.python.org/3/library/datetime.html#datetime.timedelta.total_seconds) to correctly get
the total elapsed seconds.
* `def .raise_for_status()` - **None**
* `def .raise_for_status()` - **Response**
* `def .json()` - **Any**
* `def .read()` - **bytes**
* `def .iter_raw([chunk_size])` - **bytes iterator**
3 changes: 3 additions & 0 deletions docs/async.md
Original file line number Diff line number Diff line change
@@ -53,6 +53,9 @@ async with httpx.AsyncClient() as client:
...
```

!!! warning
In order to get the most benefit from connection pooling, make sure you're not instantiating multiple client instances - for example by using `async with` inside a "hot loop". This can be achieved either by having a single scoped client that's passed throughout wherever it's needed, or by having a single global client instance.

Alternatively, use `await client.aclose()` if you want to close a client explicitly:

```python
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
@@ -144,6 +144,6 @@ To include the optional brotli decoder support, use:
$ pip install httpx[brotli]
```

HTTPX requires Python 3.7+
HTTPX requires Python 3.8+

[sync-support]: https://github.com/encode/httpx/issues/572
2 changes: 1 addition & 1 deletion docs/logging.md
Original file line number Diff line number Diff line change
@@ -41,7 +41,7 @@ DEBUG [2023-03-16 14:36:21] httpcore - connection.close.started
DEBUG [2023-03-16 14:36:21] httpcore - connection.close.complete
```

Logging output includes information from both the high-level `httpx` logger, and the network-level `httpcore` logger, which can be configured seperately.
Logging output includes information from both the high-level `httpx` logger, and the network-level `httpcore` logger, which can be configured separately.

For handling more complex logging configurations you might want to use the dictionary configuration style...

13 changes: 10 additions & 3 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
@@ -285,15 +285,22 @@ Traceback (most recent call last):
File "/Users/tomchristie/GitHub/encode/httpcore/httpx/models.py", line 837, in raise_for_status
raise HTTPStatusError(message, response=self)
httpx._exceptions.HTTPStatusError: 404 Client Error: Not Found for url: https://httpbin.org/status/404
For more information check: https://httpstatuses.com/404
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404
```

Any successful response codes will simply return `None` rather than raising an exception.
Any successful response codes will return the `Response` instance rather than raising an exception.

```pycon
>>> r.raise_for_status()
```

The method returns the response instance, allowing you to use it inline. For example:

```pycon
>>> r = httpx.get('...').raise_for_status()
>>> data = httpx.get('...').raise_for_status().json()
```

## Response Headers

The response headers are available as a dictionary-like interface.
@@ -367,7 +374,7 @@ If you're using streaming responses in any of these ways then the `response.cont

```pycon
>>> with httpx.stream("GET", "https://www.example.com") as r:
... if r.headers['Content-Length'] < TOO_LONG:
... if int(r.headers['Content-Length']) < TOO_LONG:
... r.read()
... print(r.text)
```
6 changes: 6 additions & 0 deletions docs/third_party_packages.md
Original file line number Diff line number Diff line change
@@ -6,6 +6,12 @@ As HTTPX usage grows, there is an expanding community of developers building too

<!-- NOTE: this list is in alphabetical order. -->

### Hishel

[GitHub](https://github.com/karosis88/hishel) - [Documentation](https://karosis88.github.io/hishel/)

An elegant HTTP Cache implementation for HTTPX and HTTP Core.

### Authlib

[GitHub](https://github.com/lepture/authlib) - [Documentation](https://docs.authlib.org/en/latest/)
2 changes: 1 addition & 1 deletion httpx/__version__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__title__ = "httpx"
__description__ = "A next generation HTTP client, for Python 3."
__version__ = "0.24.1"
__version__ = "0.25.0"
2 changes: 1 addition & 1 deletion httpx/_compat.py
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@
brotli = None

if sys.version_info >= (3, 10) or (
sys.version_info >= (3, 7) and ssl.OPENSSL_VERSION_INFO >= (1, 1, 0, 7)
sys.version_info >= (3, 8) and ssl.OPENSSL_VERSION_INFO >= (1, 1, 0, 7)
):

def set_minimum_tls_version_1_2(context: ssl.SSLContext) -> None:
2 changes: 2 additions & 0 deletions httpx/_config.py
Original file line number Diff line number Diff line change
@@ -326,6 +326,7 @@ def __init__(
self,
url: URLTypes,
*,
ssl_context: typing.Optional[ssl.SSLContext] = None,
auth: typing.Optional[typing.Tuple[str, str]] = None,
headers: typing.Optional[HeaderTypes] = None,
):
@@ -343,6 +344,7 @@ def __init__(
self.url = url
self.auth = auth
self.headers = headers
self.ssl_context = ssl_context

@property
def raw_auth(self) -> typing.Optional[typing.Tuple[bytes, bytes]]:
10 changes: 5 additions & 5 deletions httpx/_models.py
Original file line number Diff line number Diff line change
@@ -467,7 +467,7 @@ def __init__(
# the client will set `response.next_request`.
self.next_request: typing.Optional[Request] = None

self.extensions = {} if extensions is None else extensions
self.extensions: ResponseExtensions = {} if extensions is None else extensions
self.history = [] if history is None else list(history)

self.is_closed = False
@@ -711,7 +711,7 @@ def has_redirect_location(self) -> bool:
and "Location" in self.headers
)

def raise_for_status(self) -> None:
def raise_for_status(self) -> "Response":
"""
Raise the `HTTPStatusError` if one occurred.
"""
@@ -723,18 +723,18 @@ def raise_for_status(self) -> None:
)

if self.is_success:
return
return self

if self.has_redirect_location:
message = (
"{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
"Redirect location: '{0.headers[location]}'\n"
"For more information check: https://httpstatuses.com/{0.status_code}"
"For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}"
)
else:
message = (
"{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
"For more information check: https://httpstatuses.com/{0.status_code}"
"For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}"
)

status_class = self.status_code // 100
8 changes: 7 additions & 1 deletion httpx/_transports/asgi.py
Original file line number Diff line number Diff line change
@@ -161,9 +161,15 @@ async def send(message: typing.Dict[str, typing.Any]) -> None:
try:
await self.app(scope, receive, send)
except Exception: # noqa: PIE-786
if self.raise_app_exceptions or not response_complete.is_set():
if self.raise_app_exceptions:
raise

response_complete.set()
if status_code is None:
status_code = 500
if response_headers is None:
response_headers = {}

assert response_complete.is_set()
assert status_code is not None
assert response_headers is not None
13 changes: 13 additions & 0 deletions httpx/_transports/default.py
Original file line number Diff line number Diff line change
@@ -53,6 +53,12 @@
T = typing.TypeVar("T", bound="HTTPTransport")
A = typing.TypeVar("A", bound="AsyncHTTPTransport")

SOCKET_OPTION = typing.Union[
typing.Tuple[int, int, int],
typing.Tuple[int, int, typing.Union[bytes, bytearray]],
typing.Tuple[int, int, None, int],
]


@contextlib.contextmanager
def map_httpcore_exceptions() -> typing.Iterator[None]:
@@ -122,6 +128,7 @@ def __init__(
uds: typing.Optional[str] = None,
local_address: typing.Optional[str] = None,
retries: int = 0,
socket_options: typing.Optional[typing.Iterable[SOCKET_OPTION]] = None,
) -> None:
ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env)

@@ -136,6 +143,7 @@ def __init__(
uds=uds,
local_address=local_address,
retries=retries,
socket_options=socket_options,
)
elif proxy.url.scheme in ("http", "https"):
self._pool = httpcore.HTTPProxy(
@@ -148,11 +156,13 @@ def __init__(
proxy_auth=proxy.raw_auth,
proxy_headers=proxy.headers.raw,
ssl_context=ssl_context,
proxy_ssl_context=proxy.ssl_context,
max_connections=limits.max_connections,
max_keepalive_connections=limits.max_keepalive_connections,
keepalive_expiry=limits.keepalive_expiry,
http1=http1,
http2=http2,
socket_options=socket_options,
)
elif proxy.url.scheme == "socks5":
try:
@@ -257,6 +267,7 @@ def __init__(
uds: typing.Optional[str] = None,
local_address: typing.Optional[str] = None,
retries: int = 0,
socket_options: typing.Optional[typing.Iterable[SOCKET_OPTION]] = None,
) -> None:
ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env)

@@ -271,6 +282,7 @@ def __init__(
uds=uds,
local_address=local_address,
retries=retries,
socket_options=socket_options,
)
elif proxy.url.scheme in ("http", "https"):
self._pool = httpcore.AsyncHTTPProxy(
@@ -288,6 +300,7 @@ def __init__(
keepalive_expiry=limits.keepalive_expiry,
http1=http1,
http2=http2,
socket_options=socket_options,
)
elif proxy.url.scheme == "socks5":
try:
1 change: 1 addition & 0 deletions httpx/_transports/wsgi.py
Original file line number Diff line number Diff line change
@@ -102,6 +102,7 @@ def handle_request(self, request: Request) -> Response:
"QUERY_STRING": request.url.query.decode("ascii"),
"SERVER_NAME": request.url.host,
"SERVER_PORT": str(port),
"SERVER_PROTOCOL": "HTTP/1.1",
"REMOTE_ADDR": self.remote_addr,
}
for header_key, header_value in request.headers.raw:
5 changes: 3 additions & 2 deletions httpx/_types.py
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@
Iterator,
List,
Mapping,
MutableMapping,
NamedTuple,
Optional,
Sequence,
@@ -87,7 +88,7 @@

RequestContent = Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]]
ResponseContent = Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]]
ResponseExtensions = Mapping[str, Any]
ResponseExtensions = MutableMapping[str, Any]

RequestData = Mapping[str, Any]

@@ -104,7 +105,7 @@
]
RequestFiles = Union[Mapping[str, FileTypes], Sequence[Tuple[str, FileTypes]]]

RequestExtensions = Mapping[str, Any]
RequestExtensions = MutableMapping[str, Any]


class SyncByteStream:
Loading