httpunk 0.1.0a2__tar.gz → 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {httpunk-0.1.0a2 → httpunk-0.1.1}/Cargo.lock +11 -11
- {httpunk-0.1.0a2 → httpunk-0.1.1}/Cargo.toml +1 -1
- {httpunk-0.1.0a2 → httpunk-0.1.1}/PKG-INFO +2 -2
- {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/_backend/asyncio.py +21 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/_backend/tonio.py +7 -2
- {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/h1/server.py +49 -15
- {httpunk-0.1.0a2 → httpunk-0.1.1}/pyproject.toml +1 -1
- {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_h1_server.py +29 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/LICENSE +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/README.md +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/Cargo.toml +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/LICENSE +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/UPSTREAM_VERSION +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/codec/error.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/codec/mod.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/ext.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/frame/data.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/frame/go_away.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/frame/head.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/frame/headers.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/frame/mod.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/frame/ping.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/frame/priority.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/frame/reason.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/frame/reset.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/frame/settings.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/frame/stream_id.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/frame/util.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/frame/window_update.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/hpack/decoder.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/hpack/encoder.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/hpack/header.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/hpack/huffman/mod.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/hpack/huffman/table.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/hpack/mod.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/hpack/table.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/lib.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/proto/error.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/proto/mod.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/proto/streams/flow_control.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/proto/streams/mod.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/proto/streams/state.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/Cargo.toml +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/LICENSE +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/UPSTREAM_VERSION +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/body/length.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/body/mod.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/cfg.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/common/date.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/common/mod.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/error.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/ext/h1_reason_phrase.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/ext/informational.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/ext/mod.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/headers.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/lib.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/proto/h1/decode.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/proto/h1/encode.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/proto/h1/httpunk.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/proto/h1/io.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/proto/h1/mod.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/proto/h1/role.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/proto/mod.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/trace.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper-util/Cargo.toml +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper-util/LICENSE +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper-util/UPSTREAM_VERSION +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper-util/src/client/mod.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper-util/src/client/proxy/matcher.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper-util/src/client/proxy/mod.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper-util/src/lib.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/__init__.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/_backend/__init__.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/_common.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/_httpunk.pyi +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/asyncio.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/exceptions.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/h1/__init__.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/h1/client.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/h1/connection.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/h1/share.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/h2/__init__.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/h2/client.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/h2/connection.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/h2/server.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/h2/settings.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/h2/share.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/h2/stream.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/h2/streams.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/http.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/py.typed +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/types.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/util/__init__.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/util/auto.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/util/client.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/util/graceful.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/util/pool.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/util/proxy.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/src/errors.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/src/h1/codec.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/src/h1/mod.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/src/h2/codec.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/src/h2/mod.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/src/h2/streams.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/src/http/mod.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/src/lib.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/src/proxy.rs +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/_client.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/conftest.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_asyncio_backend.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_asyncio_client_protocol.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_asyncio_e2e.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_asyncio_protocol.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_backend.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_h1_codec.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_h1_conn.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_h1_decode.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_h2_codec.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_h2_conn.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_h2_errors.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_h2_flow.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_h2_flow_control.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_h2_ready.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_h2_server.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_h2_settings.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_h2_state.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_util_auto.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_util_connect.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_util_graceful.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_util_pool.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_util_proxy.py +0 -0
- {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_util_tls.py +0 -0
|
@@ -16,9 +16,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
|
|
16
16
|
|
|
17
17
|
[[package]]
|
|
18
18
|
name = "bytes"
|
|
19
|
-
version = "1.
|
|
19
|
+
version = "1.12.1"
|
|
20
20
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
21
|
-
checksum = "
|
|
21
|
+
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
|
22
22
|
|
|
23
23
|
[[package]]
|
|
24
24
|
name = "equivalent"
|
|
@@ -86,9 +86,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
|
|
86
86
|
|
|
87
87
|
[[package]]
|
|
88
88
|
name = "http"
|
|
89
|
-
version = "1.4.
|
|
89
|
+
version = "1.4.2"
|
|
90
90
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
91
|
-
checksum = "
|
|
91
|
+
checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
|
|
92
92
|
dependencies = [
|
|
93
93
|
"bytes",
|
|
94
94
|
"itoa",
|
|
@@ -118,7 +118,7 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
|
|
118
118
|
|
|
119
119
|
[[package]]
|
|
120
120
|
name = "httpunk"
|
|
121
|
-
version = "0.1.
|
|
121
|
+
version = "0.1.1"
|
|
122
122
|
dependencies = [
|
|
123
123
|
"bytes",
|
|
124
124
|
"h2",
|
|
@@ -175,9 +175,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
|
|
175
175
|
|
|
176
176
|
[[package]]
|
|
177
177
|
name = "libc"
|
|
178
|
-
version = "0.2.
|
|
178
|
+
version = "0.2.186"
|
|
179
179
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
180
|
-
checksum = "
|
|
180
|
+
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
|
181
181
|
|
|
182
182
|
[[package]]
|
|
183
183
|
name = "once_cell"
|
|
@@ -271,9 +271,9 @@ dependencies = [
|
|
|
271
271
|
|
|
272
272
|
[[package]]
|
|
273
273
|
name = "quote"
|
|
274
|
-
version = "1.0.
|
|
274
|
+
version = "1.0.46"
|
|
275
275
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
276
|
-
checksum = "
|
|
276
|
+
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
|
277
277
|
dependencies = [
|
|
278
278
|
"proc-macro2",
|
|
279
279
|
]
|
|
@@ -292,9 +292,9 @@ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
|
|
292
292
|
|
|
293
293
|
[[package]]
|
|
294
294
|
name = "syn"
|
|
295
|
-
version = "2.0.
|
|
295
|
+
version = "2.0.118"
|
|
296
296
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
297
|
-
checksum = "
|
|
297
|
+
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
|
298
298
|
dependencies = [
|
|
299
299
|
"proc-macro2",
|
|
300
300
|
"quote",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: httpunk
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: BSD License
|
|
@@ -19,7 +19,7 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
|
19
19
|
Classifier: Programming Language :: Python
|
|
20
20
|
Classifier: Programming Language :: Rust
|
|
21
21
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
22
|
-
Requires-Dist: tonio~=0.
|
|
22
|
+
Requires-Dist: tonio~=0.8.1 ; python_full_version >= '3.14' and extra == 'tonio'
|
|
23
23
|
Provides-Extra: tonio
|
|
24
24
|
License-File: LICENSE
|
|
25
25
|
Summary: The Rust HTTP library for Python
|
|
@@ -162,6 +162,12 @@ class _AsyncioStream(asyncio.Protocol):
|
|
|
162
162
|
if waiter is not None and not waiter.done():
|
|
163
163
|
waiter.set_result(None)
|
|
164
164
|
|
|
165
|
+
def interrupt_read(self):
|
|
166
|
+
"""Wake a parked `receive_some` so it returns b"" — lets a graceful shutdown release
|
|
167
|
+
an idle keep-alive read WITHOUT racing a coroutine (asyncio can wake a parked read from
|
|
168
|
+
another task; tonio can't, which is why it uses a select-based race instead)."""
|
|
169
|
+
self._wake_reader()
|
|
170
|
+
|
|
165
171
|
|
|
166
172
|
class _AsyncioScope:
|
|
167
173
|
"""A nursery over `asyncio` tasks, matching tonio's scope surface: `spawn`,
|
|
@@ -297,3 +303,18 @@ class AsyncioBackend:
|
|
|
297
303
|
scope = _AsyncioScope
|
|
298
304
|
monotonic = staticmethod(_time.monotonic)
|
|
299
305
|
sleep = staticmethod(asyncio.sleep) # async sleep(seconds), for deadline races (`select`)
|
|
306
|
+
|
|
307
|
+
# Every backend implements `timeout` (above), so the h1 server always uses it for the
|
|
308
|
+
# head-read deadline. This extra capability is asyncio-only: `_AsyncioStream.interrupt_read`
|
|
309
|
+
# can wake a parked idle read from another task, so the h1 server can skip the per-read
|
|
310
|
+
# shutdown `select`. tonio can't (no such method) → it keeps the select there.
|
|
311
|
+
native_read_interrupt = True
|
|
312
|
+
|
|
313
|
+
async def timeout(self, coro, seconds):
|
|
314
|
+
"""Run `coro` bounded by `seconds`; return `(result, True)` on completion, or
|
|
315
|
+
`(None, False)` on timeout (the coro is cancelled). One task + one timer, far cheaper
|
|
316
|
+
than racing a `sleep()` coroutine via `select`."""
|
|
317
|
+
try:
|
|
318
|
+
return await asyncio.wait_for(coro, seconds), True
|
|
319
|
+
except (TimeoutError, asyncio.TimeoutError):
|
|
320
|
+
return None, False
|
|
@@ -12,7 +12,7 @@ from tonio.colored.net import open_tcp_stream as _open_tcp_stream
|
|
|
12
12
|
from tonio.colored.net.tls import open_tls_over_tcp_stream as _open_tls_over_tcp_stream
|
|
13
13
|
from tonio.colored.sync import Lock as _Lock, Semaphore as _Semaphore
|
|
14
14
|
from tonio.colored.sync.channel import unbounded as _unbounded
|
|
15
|
-
from tonio.colored.time import time as _now
|
|
15
|
+
from tonio.colored.time import time as _now, timeout as _timeout
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class _TonioSemaphore:
|
|
@@ -107,4 +107,9 @@ class TonioBackend:
|
|
|
107
107
|
semaphore = _TonioSemaphore
|
|
108
108
|
queue = staticmethod(_unbounded)
|
|
109
109
|
monotonic = staticmethod(_now)
|
|
110
|
-
sleep = staticmethod(_colored.sleep) # async sleep(seconds)
|
|
110
|
+
sleep = staticmethod(_colored.sleep) # async sleep(seconds)
|
|
111
|
+
# `timeout(coro, seconds) -> (result, completed)` — tonio's native deadline; the h1 server
|
|
112
|
+
# uses it to bound the request-head read (measured ~12-16% faster than racing sleep() via
|
|
113
|
+
# select). Same contract as AsyncioBackend.timeout. tonio still can't wake a parked recv from
|
|
114
|
+
# another task, so it does NOT advertise `native_read_interrupt` (idle shutdown stays select).
|
|
115
|
+
timeout = staticmethod(_timeout)
|
|
@@ -53,7 +53,6 @@ _CONTINUE = b"HTTP/1.1 100 Continue\r\n\r\n"
|
|
|
53
53
|
# (io.rs: 8192 + 4096*100). Bounds per-connection memory against a slow/oversized head.
|
|
54
54
|
_MAX_HEAD_SIZE = 8192 + 4096 * 100
|
|
55
55
|
_DEFAULT_HEADER_READ_TIMEOUT = 30.0 # hyper's `header_read_timeout` default (http1.rs L249)
|
|
56
|
-
_TIMED_OUT = object() # sentinel: the header read exceeded `header_read_timeout`
|
|
57
56
|
# The HTTP/2 connection preface (client prior-knowledge). An h1 server that sees it
|
|
58
57
|
# closes silently with a version error rather than writing a 400 (hyper `on_parse_error`
|
|
59
58
|
# -> `has_h2_prefix` -> `new_version_h2`, conn.rs L29/L809-812).
|
|
@@ -174,6 +173,22 @@ class ServerRequest:
|
|
|
174
173
|
self._responded = True
|
|
175
174
|
await self._conn.send_response(self, status, headers, body)
|
|
176
175
|
|
|
176
|
+
def detach(self) -> bytes:
|
|
177
|
+
"""Take over the raw connection for a protocol upgrade (WebSocket, or a custom protocol):
|
|
178
|
+
stop the server's accept loop and relinquish the transport WITHOUT closing it, returning
|
|
179
|
+
any bytes already read past the request head (to replay). The caller — which supplied the
|
|
180
|
+
transport to `H1Server` — owns it afterwards and must drive the upgrade itself (httpunk
|
|
181
|
+
sends no response). Like httpunk's 101/CONNECT tunnel hand-off, but caller-driven.
|
|
182
|
+
|
|
183
|
+
After this, the accept loop ends (`next_request` returns None) and the server's close on
|
|
184
|
+
`__aexit__` is a no-op, so the transport stays open. cf. Go's `http.Hijacker`."""
|
|
185
|
+
if self._responded:
|
|
186
|
+
raise RuntimeError("cannot detach: a response was already sent for this request")
|
|
187
|
+
self._responded = True
|
|
188
|
+
leftover = self._decoder.take_buffered()
|
|
189
|
+
self._conn._detach()
|
|
190
|
+
return leftover
|
|
191
|
+
|
|
177
192
|
def __repr__(self) -> str:
|
|
178
193
|
return f"ServerRequest(method={self.method!r}, target={self.target!r})"
|
|
179
194
|
|
|
@@ -193,6 +208,12 @@ class ServerConnection(H1ConnectionBase):
|
|
|
193
208
|
# Max time to read a complete request head before closing (slowloris defence),
|
|
194
209
|
# hyper's `header_read_timeout` (default 30s, http1.rs L249). `None` disables it.
|
|
195
210
|
self._header_read_timeout = header_read_timeout
|
|
211
|
+
# The head-read deadline always uses `backend.timeout` (every backend implements it).
|
|
212
|
+
# This capability is extra: a backend that can wake a parked read from another task
|
|
213
|
+
# (asyncio) lets us skip the per-read shutdown `select`; absent (tonio) → use `select`.
|
|
214
|
+
# See `_read_or_shutdown` + `graceful_shutdown`.
|
|
215
|
+
self._native_read_interrupt = getattr(self.backend, "native_read_interrupt", False)
|
|
216
|
+
self._idle_read_parked = False # True only while parked in the idle between-requests read
|
|
196
217
|
|
|
197
218
|
async def start(self):
|
|
198
219
|
pass # HTTP/1 has no connection preface / handshake
|
|
@@ -206,6 +227,10 @@ class ServerConnection(H1ConnectionBase):
|
|
|
206
227
|
# loop to completion and closes; nothing is awaited/closed here.
|
|
207
228
|
self._reusable = False
|
|
208
229
|
self._shutdown_evt.set()
|
|
230
|
+
if self._native_read_interrupt and self._idle_read_parked:
|
|
231
|
+
# asyncio: wake the read parked idly between requests so it returns promptly and
|
|
232
|
+
# the connection closes, instead of relying on a per-read select race (tonio's path).
|
|
233
|
+
self.transport.interrupt_read()
|
|
209
234
|
|
|
210
235
|
def mark_unusable(self):
|
|
211
236
|
self._reusable = False
|
|
@@ -314,22 +339,17 @@ class ServerConnection(H1ConnectionBase):
|
|
|
314
339
|
return False
|
|
315
340
|
|
|
316
341
|
async def _read_request_head(self, codec, initial=b""):
|
|
317
|
-
# Bound the head read by `header_read_timeout` (slowloris defence, hyper
|
|
318
|
-
#
|
|
319
|
-
#
|
|
320
|
-
#
|
|
321
|
-
#
|
|
322
|
-
# normalized `backend.timeout(coro, seconds)` primitive — more direct, no
|
|
323
|
-
# sentinel / nested select. Kept as-is for now (see TODONOTES).
|
|
342
|
+
# Bound the head read by `header_read_timeout` (slowloris defence, hyper http1.rs L249):
|
|
343
|
+
# if the deadline wins, close with no response (hyper closes on a header-read timeout).
|
|
344
|
+
# `backend.timeout(coro, seconds) -> (result, completed)` cancels the read cleanly on
|
|
345
|
+
# expiry — cheap on every backend (asyncio: one task + one timer; tonio: its native
|
|
346
|
+
# timeout). `None` disables the deadline.
|
|
324
347
|
if self._header_read_timeout is None:
|
|
325
348
|
return await self._read_head_frames(codec, initial)
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
result = await self.backend.select(self._read_head_frames(codec, initial), _deadline())
|
|
332
|
-
if result is _TIMED_OUT:
|
|
349
|
+
result, completed = await self.backend.timeout(
|
|
350
|
+
self._read_head_frames(codec, initial), self._header_read_timeout
|
|
351
|
+
)
|
|
352
|
+
if not completed:
|
|
333
353
|
self._closed = True
|
|
334
354
|
self._reusable = False
|
|
335
355
|
self._close_transport()
|
|
@@ -390,6 +410,20 @@ class ServerConnection(H1ConnectionBase):
|
|
|
390
410
|
if self._shutdown_evt.is_set():
|
|
391
411
|
return _SHUTDOWN
|
|
392
412
|
|
|
413
|
+
if self._native_read_interrupt:
|
|
414
|
+
# asyncio: a plain read; if it parks, graceful_shutdown() wakes it via
|
|
415
|
+
# `interrupt_read` (a shutdown-woken read returns b"") — no per-read select race.
|
|
416
|
+
self._idle_read_parked = True
|
|
417
|
+
try:
|
|
418
|
+
data = await self.transport.receive_some(n)
|
|
419
|
+
finally:
|
|
420
|
+
self._idle_read_parked = False
|
|
421
|
+
if not data and self._shutdown_evt.is_set():
|
|
422
|
+
return _SHUTDOWN
|
|
423
|
+
return data
|
|
424
|
+
|
|
425
|
+
# Backends without native read control (tonio can't wake a parked recv from another
|
|
426
|
+
# task): race the read against the shutdown signal via `select`.
|
|
393
427
|
def _recv():
|
|
394
428
|
return self.transport.receive_some(n)
|
|
395
429
|
|
|
@@ -276,6 +276,35 @@ async def test_server_header_read_timeout_closes_slow_head():
|
|
|
276
276
|
s.cancel()
|
|
277
277
|
|
|
278
278
|
|
|
279
|
+
@pytest.mark.tonio
|
|
280
|
+
async def test_server_detach_hands_off_open_connection():
|
|
281
|
+
"""detach() stops the accept loop and relinquishes the transport WITHOUT closing it,
|
|
282
|
+
returning bytes read past the request head — so a caller can take over the raw connection
|
|
283
|
+
for a protocol upgrade (e.g. WebSocket)."""
|
|
284
|
+
listener, host, port = await _listener()
|
|
285
|
+
captured = {}
|
|
286
|
+
|
|
287
|
+
async def serve():
|
|
288
|
+
transport = await listener.accept()
|
|
289
|
+
async with H1Server(transport) as server:
|
|
290
|
+
async for req in server:
|
|
291
|
+
captured["leftover"] = req.detach() # loop ends next (detached), socket stays open
|
|
292
|
+
# `__aexit__` ran (close() is a no-op after detach) — the transport is still usable, so
|
|
293
|
+
# the caller drives its own upgrade response on the raw connection.
|
|
294
|
+
await transport.send_all(b"HTTP/1.1 101 Switching Protocols\r\nupgrade: custom\r\n\r\nOWNED")
|
|
295
|
+
|
|
296
|
+
async with scope() as s:
|
|
297
|
+
s.spawn(serve())
|
|
298
|
+
client = await _raw_client(host, port)
|
|
299
|
+
# request head + trailing bytes the client sent past it (must survive as `leftover`)
|
|
300
|
+
await client.send_all(b"GET /up HTTP/1.1\r\nhost: x\r\nupgrade: custom\r\nconnection: upgrade\r\n\r\nEXTRA")
|
|
301
|
+
data = await _read_until(client, b"OWNED")
|
|
302
|
+
s.cancel()
|
|
303
|
+
|
|
304
|
+
assert captured["leftover"] == b"EXTRA" # bytes past the head handed back
|
|
305
|
+
assert b"101 Switching Protocols" in data and data.endswith(b"OWNED") # socket stayed open
|
|
306
|
+
|
|
307
|
+
|
|
279
308
|
@pytest.mark.tonio
|
|
280
309
|
async def test_server_no_100_continue_for_http10():
|
|
281
310
|
"""An HTTP/1.0 request never gets an auto 100-continue, even with `Expect:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|