Skip to content

Commit 1933840

Browse files
authored
Release 3.10 (#159)
3.10.0 (2024-10-21) ------------------ **Added** - Automatic Advanced Keep-Alive for HTTP/2 and HTTP/3 over QUIC by sending PING frames. New Session, and Adapter parameters are now available: `keepalive_delay`, and `keepalive_idle_window`. This greatly improves your daily experience working with HTTP/2+ remote peers. **Fixed** - Unshielded picotls assertion error in Python < 3.10 when trying to fetch the peer intermediate certificate. (#157)
2 parents 409718b + 6d19929 commit 1933840

12 files changed

+149
-27
lines changed

.pre-commit-config.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,4 @@ repos:
2828
- id: mypy
2929
args: [--check-untyped-defs]
3030
exclude: 'tests/|noxfile.py'
31-
additional_dependencies: ['charset_normalizer', 'urllib3.future>=2.10.903', 'wassima>=1.0.1', 'idna', 'kiss_headers']
31+
additional_dependencies: ['charset_normalizer', 'urllib3.future>=2.11.900', 'wassima>=1.0.1', 'idna', 'kiss_headers']

HISTORY.md

+11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
Release History
22
===============
33

4+
3.10.0 (2024-10-21)
5+
------------------
6+
7+
**Added**
8+
- Automatic Advanced Keep-Alive for HTTP/2 and HTTP/3 over QUIC by sending PING frames.
9+
New Session, and Adapter parameters are now available: `keepalive_delay`, and `keepalive_idle_window`.
10+
This greatly improves your daily experience working with HTTP/2+ remote peers.
11+
12+
**Fixed**
13+
- Unshielded picotls assertion error in Python < 3.10 when trying to fetch the peer intermediate certificate.
14+
415
3.9.1 (2024-10-13)
516
------------------
617

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Niquests, is the “**Safest**, **Fastest[^10]**, **Easiest**, and **Most advanc
4545
| `Early Responses` |||||
4646
| `WebSocket over HTTP/1` ||[^14] |[^14] ||
4747
| `WebSocket over HTTP/2 and HTTP/3` |[^13] ||||
48+
| `Automatic Ping for HTTP/2+` || N/A || N/A |
4849
| `Automatic Connection Upgrade / Downgrade` || N/A || N/A |
4950
</details>
5051

docs/user/quickstart.rst

+26
Original file line numberDiff line numberDiff line change
@@ -1249,6 +1249,32 @@ See::
12491249

12501250
.. note:: The given example are really basic ones. You may adjust at will the settings and algorithm to match your requisites.
12511251

1252+
Keep-Alive
1253+
----------
1254+
1255+
.. note:: Available since Niquests v3.10 and before this only HTTP/1.1 were kept alive properly.
1256+
1257+
Niquests can automatically make sure that your HTTP connection is kept alive
1258+
no matter the used protocol using a discrete scheduled task for each host.
1259+
1260+
.. code-block:: python
1261+
1262+
import niquests
1263+
1264+
sess = niquests.Session(keepalive_delay=300, keepalive_idle_window=60) # already the defaults!, you don't need to specify anything
1265+
1266+
In that example, we indicate that we wish to keep a connection alive for 5 minutes and
1267+
eventually send ping every 60s after the connection was idle. (Those values are the default ones!)
1268+
1269+
The pings are only sent when using HTTP/2 or HTTP/3 over QUIC. Any connection activity is considered as used, therefor
1270+
making the ping only 60s after zero activity. If the connection receive unsolicited data, it is also considered used.
1271+
1272+
.. note:: Setting either keepalive_delay or keepalive_idle_window to None disable this feature.
1273+
1274+
.. warning:: We do not recommend setting anything lower than 30s for keepalive_idle_window. Anything lower than 1s is considered to be 1s. High frequency ping will lower the performance of your connection pool. And probably end up by getting kicked out by the server.
1275+
1276+
Once the ``keepalive_delay`` passed, we do not close the connection, we simply cease to ensure it is alive. This is purely for backward compatibility with our predecessor, as some host may retain the connection for hours.
1277+
12521278
-----------------------
12531279

12541280
Ready for more? Check out the :ref:`advanced <advanced>` section.

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ dynamic = ["version"]
4242
dependencies = [
4343
"charset_normalizer>=2,<4",
4444
"idna>=2.5,<4",
45-
"urllib3.future>=2.10.904,<3",
45+
"urllib3.future>=2.11.900,<3",
4646
"wassima>=1.0.1,<2",
4747
"kiss_headers>=2,<4",
4848
]

src/niquests/__version__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
__url__: str = "https://niquests.readthedocs.io"
1010

1111
__version__: str
12-
__version__ = "3.9.1"
12+
__version__ = "3.10.0"
1313

14-
__build__: int = 0x030901
14+
__build__: int = 0x031000
1515
__author__: str = "Kenneth Reitz"
1616
__author_email__: str = "me@kennethreitz.org"
1717
__license__: str = "Apache-2.0"

src/niquests/_async.py

+13
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ def __init__(
130130
pool_connections: int = DEFAULT_POOLSIZE,
131131
pool_maxsize: int = DEFAULT_POOLSIZE,
132132
happy_eyeballs: bool | int = False,
133+
keepalive_delay: float | int | None = 300.0,
134+
keepalive_idle_window: float | int | None = 60.0,
133135
):
134136
if [disable_ipv4, disable_ipv6].count(True) == 2:
135137
raise RuntimeError("Cannot disable both IPv4 and IPv6")
@@ -195,6 +197,9 @@ def __init__(
195197

196198
self._happy_eyeballs = happy_eyeballs
197199

200+
self._keepalive_delay = keepalive_delay
201+
self._keepalive_idle_window = keepalive_idle_window
202+
198203
#: SSL Verification default.
199204
#: Defaults to `True`, requiring requests to verify the TLS certificate at the
200205
#: remote end.
@@ -253,6 +258,8 @@ def __init__(
253258
pool_connections=pool_connections,
254259
pool_maxsize=pool_maxsize,
255260
happy_eyeballs=happy_eyeballs,
261+
keepalive_delay=keepalive_delay,
262+
keepalive_idle_window=keepalive_idle_window,
256263
),
257264
)
258265
self.mount(
@@ -269,6 +276,8 @@ def __init__(
269276
pool_connections=pool_connections,
270277
pool_maxsize=pool_maxsize,
271278
happy_eyeballs=happy_eyeballs,
279+
keepalive_delay=keepalive_delay,
280+
keepalive_idle_window=keepalive_idle_window,
272281
),
273282
)
274283

@@ -436,6 +445,8 @@ async def on_early_response(early_response: Response) -> None:
436445
pool_connections=self._pool_connections,
437446
pool_maxsize=self._pool_maxsize,
438447
happy_eyeballs=self._happy_eyeballs,
448+
keepalive_delay=self._keepalive_delay,
449+
keepalive_idle_window=self._keepalive_idle_window,
439450
),
440451
)
441452
self.mount(
@@ -452,6 +463,8 @@ async def on_early_response(early_response: Response) -> None:
452463
pool_connections=self._pool_connections,
453464
pool_maxsize=self._pool_maxsize,
454465
happy_eyeballs=self._happy_eyeballs,
466+
keepalive_delay=self._keepalive_delay,
467+
keepalive_idle_window=self._keepalive_idle_window,
455468
),
456469
)
457470

src/niquests/adapters.py

+21-1
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,8 @@ class HTTPAdapter(BaseAdapter):
333333
"_disable_ipv4",
334334
"_disable_ipv6",
335335
"_happy_eyeballs",
336+
"_keepalive_delay",
337+
"_keepalive_idle_window",
336338
]
337339

338340
def __init__(
@@ -341,7 +343,7 @@ def __init__(
341343
pool_maxsize: int = DEFAULT_POOLSIZE,
342344
max_retries: RetryType = DEFAULT_RETRIES,
343345
pool_block: bool = DEFAULT_POOLBLOCK,
344-
*, # todo: revert if any complaint about it... :s
346+
*,
345347
quic_cache_layer: CacheLayerAltSvcType | None = None,
346348
disable_http1: bool = False,
347349
disable_http2: bool = False,
@@ -352,6 +354,8 @@ def __init__(
352354
disable_ipv4: bool = False,
353355
disable_ipv6: bool = False,
354356
happy_eyeballs: bool | int = False,
357+
keepalive_delay: float | int | None = 300.0,
358+
keepalive_idle_window: float | int | None = 60.0,
355359
):
356360
if isinstance(max_retries, bool):
357361
self.max_retries: RetryType = False
@@ -383,6 +387,8 @@ def __init__(
383387
self._disable_ipv4 = disable_ipv4
384388
self._disable_ipv6 = disable_ipv6
385389
self._happy_eyeballs = happy_eyeballs
390+
self._keepalive_delay = keepalive_delay
391+
self._keepalive_idle_window = keepalive_idle_window
386392

387393
#: we keep a list of pending (lazy) response
388394
self._promises: dict[str, Response] = {}
@@ -413,6 +419,8 @@ def __init__(
413419
source_address=source_address,
414420
socket_family=resolve_socket_family(disable_ipv4, disable_ipv6),
415421
happy_eyeballs=happy_eyeballs,
422+
keepalive_delay=keepalive_delay,
423+
keepalive_idle_window=keepalive_idle_window,
416424
)
417425

418426
def __getstate__(self) -> dict[str, typing.Any | None]:
@@ -447,6 +455,8 @@ def __setstate__(self, state):
447455
source_address=self._source_address,
448456
socket_family=resolve_socket_family(self._disable_ipv4, self._disable_ipv6),
449457
happy_eyeballs=self._happy_eyeballs,
458+
keepalive_delay=self._keepalive_delay,
459+
keepalive_idle_window=self._keepalive_idle_window,
450460
)
451461

452462
def init_poolmanager(
@@ -1335,6 +1345,8 @@ class AsyncHTTPAdapter(AsyncBaseAdapter):
13351345
"_disable_ipv4",
13361346
"_disable_ipv6",
13371347
"_happy_eyeballs",
1348+
"_keepalive_delay",
1349+
"_keepalive_idle_window",
13381350
]
13391351

13401352
def __init__(
@@ -1354,6 +1366,8 @@ def __init__(
13541366
disable_ipv4: bool = False,
13551367
disable_ipv6: bool = False,
13561368
happy_eyeballs: bool | int = False,
1369+
keepalive_delay: float | int | None = 300.0,
1370+
keepalive_idle_window: float | int | None = 60.0,
13571371
):
13581372
if isinstance(max_retries, bool):
13591373
self.max_retries: RetryType = False
@@ -1386,6 +1400,8 @@ def __init__(
13861400
self._disable_ipv4 = disable_ipv4
13871401
self._disable_ipv6 = disable_ipv6
13881402
self._happy_eyeballs = happy_eyeballs
1403+
self._keepalive_delay = keepalive_delay
1404+
self._keepalive_idle_window = keepalive_idle_window
13891405

13901406
#: we keep a list of pending (lazy) response
13911407
self._promises: dict[str, Response | AsyncResponse] = {}
@@ -1415,6 +1431,8 @@ def __init__(
14151431
source_address=source_address,
14161432
socket_family=resolve_socket_family(disable_ipv4, disable_ipv6),
14171433
happy_eyeballs=happy_eyeballs,
1434+
keepalive_delay=keepalive_delay,
1435+
keepalive_idle_window=keepalive_idle_window,
14181436
)
14191437

14201438
def __getstate__(self) -> dict[str, typing.Any | None]:
@@ -1449,6 +1467,8 @@ def __setstate__(self, state):
14491467
source_address=self._source_address,
14501468
socket_family=resolve_socket_family(self._disable_ipv4, self._disable_ipv6),
14511469
happy_eyeballs=self._happy_eyeballs,
1470+
keepalive_delay=self._keepalive_delay,
1471+
keepalive_idle_window=self._keepalive_idle_window,
14521472
)
14531473

14541474
def init_poolmanager(

src/niquests/extensions/_async_ocsp.py

+14-6
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
async_recv_tls,
5252
async_recv_tls_and_decrypt,
5353
async_send_tls,
54+
PicoTLSException,
5455
)
5556
from ._ocsp import (
5657
_str_fingerprint_of,
@@ -73,7 +74,11 @@ async def _ask_nicely_for_issuer(
7374
sock = AsyncSocket(socket.AF_INET6, socket.SOCK_STREAM)
7475

7576
sock.settimeout(timeout)
76-
await sock.connect(dst_address)
77+
78+
try:
79+
await sock.connect(dst_address)
80+
except (OSError, socket.timeout, TimeoutError, ConnectionError) as e:
81+
raise PicoTLSException from e
7782

7883
SECP256R1_P = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
7984
SECP256R1_A = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC
@@ -408,11 +413,14 @@ async def verify(
408413
raise ValueError
409414

410415
if not proxies:
411-
issuer_certificate = await _ask_nicely_for_issuer(
412-
url_parsed.hostname,
413-
conn_info.destination_address,
414-
timeout,
415-
)
416+
try:
417+
issuer_certificate = await _ask_nicely_for_issuer(
418+
url_parsed.hostname,
419+
conn_info.destination_address,
420+
timeout,
421+
)
422+
except PicoTLSException:
423+
issuer_certificate = None
416424
else:
417425
issuer_certificate = None
418426

src/niquests/extensions/_ocsp.py

+13-6
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
recv_tls,
5050
recv_tls_and_decrypt,
5151
send_tls,
52+
PicoTLSException,
5253
)
5354

5455

@@ -80,8 +81,11 @@ def _ask_nicely_for_issuer(
8081
else:
8182
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
8283

83-
sock.connect(dst_address)
8484
sock.settimeout(timeout)
85+
try:
86+
sock.connect(dst_address)
87+
except (OSError, socket.timeout, TimeoutError, ConnectionError) as e:
88+
raise PicoTLSException from e
8589

8690
SECP256R1_P = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
8791
SECP256R1_A = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC
@@ -402,11 +406,14 @@ def verify(
402406
raise ValueError
403407

404408
if not proxies:
405-
issuer_certificate = _ask_nicely_for_issuer(
406-
url_parsed.hostname,
407-
conn_info.destination_address,
408-
timeout,
409-
)
409+
try:
410+
issuer_certificate = _ask_nicely_for_issuer(
411+
url_parsed.hostname,
412+
conn_info.destination_address,
413+
timeout,
414+
)
415+
except PicoTLSException:
416+
issuer_certificate = None
410417
else:
411418
issuer_certificate = None
412419

0 commit comments

Comments
 (0)