httpunk 0.1.5__tar.gz → 0.2.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.5 → httpunk-0.2.1}/Cargo.lock +3 -3
- {httpunk-0.1.5 → httpunk-0.2.1}/Cargo.toml +2 -2
- {httpunk-0.1.5 → httpunk-0.2.1}/PKG-INFO +5 -4
- {httpunk-0.1.5 → httpunk-0.2.1}/README.md +1 -1
- httpunk-0.2.1/crates/vendor-h2/UPSTREAM_VERSION +1 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/src/hpack/encoder.rs +35 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/httpunk/_backend/asyncio.py +18 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/httpunk/_backend/tonio.py +22 -1
- {httpunk-0.1.5 → httpunk-0.2.1}/httpunk/exceptions.py +9 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/httpunk/h1/client.py +250 -26
- {httpunk-0.1.5 → httpunk-0.2.1}/httpunk/h1/connection.py +1 -1
- {httpunk-0.1.5 → httpunk-0.2.1}/httpunk/h1/server.py +13 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/httpunk/h2/client.py +21 -2
- {httpunk-0.1.5 → httpunk-0.2.1}/httpunk/h2/connection.py +159 -63
- {httpunk-0.1.5 → httpunk-0.2.1}/httpunk/h2/server.py +26 -5
- {httpunk-0.1.5 → httpunk-0.2.1}/httpunk/h2/share.py +25 -4
- {httpunk-0.1.5 → httpunk-0.2.1}/httpunk/h2/stream.py +10 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/httpunk/h2/streams.py +141 -38
- {httpunk-0.1.5 → httpunk-0.2.1}/httpunk/util/pool.py +12 -6
- {httpunk-0.1.5 → httpunk-0.2.1}/pyproject.toml +3 -2
- {httpunk-0.1.5 → httpunk-0.2.1}/tests/test_asyncio_e2e.py +41 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/tests/test_h1_conn.py +99 -5
- {httpunk-0.1.5 → httpunk-0.2.1}/tests/test_h1_server.py +130 -55
- {httpunk-0.1.5 → httpunk-0.2.1}/tests/test_h2_errors.py +28 -7
- {httpunk-0.1.5 → httpunk-0.2.1}/tests/test_h2_flow.py +113 -14
- {httpunk-0.1.5 → httpunk-0.2.1}/tests/test_h2_server.py +175 -138
- {httpunk-0.1.5 → httpunk-0.2.1}/tests/test_util_tls.py +11 -1
- httpunk-0.1.5/crates/vendor-h2/UPSTREAM_VERSION +0 -1
- {httpunk-0.1.5 → httpunk-0.2.1}/LICENSE +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/Cargo.toml +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/LICENSE +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/src/codec/error.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/src/codec/mod.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/src/ext.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/src/frame/data.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/src/frame/go_away.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/src/frame/head.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/src/frame/headers.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/src/frame/mod.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/src/frame/ping.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/src/frame/priority.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/src/frame/reason.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/src/frame/reset.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/src/frame/settings.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/src/frame/stream_id.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/src/frame/util.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/src/frame/window_update.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/src/hpack/decoder.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/src/hpack/header.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/src/hpack/huffman/mod.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/src/hpack/huffman/table.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/src/hpack/mod.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/src/hpack/table.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/src/lib.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/src/proto/error.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/src/proto/mod.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/src/proto/streams/flow_control.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/src/proto/streams/mod.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-h2/src/proto/streams/state.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-hyper/Cargo.toml +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-hyper/LICENSE +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-hyper/UPSTREAM_VERSION +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-hyper/src/body/length.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-hyper/src/body/mod.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-hyper/src/cfg.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-hyper/src/common/date.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-hyper/src/common/mod.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-hyper/src/error.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-hyper/src/ext/h1_reason_phrase.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-hyper/src/ext/informational.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-hyper/src/ext/mod.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-hyper/src/headers.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-hyper/src/lib.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-hyper/src/proto/h1/decode.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-hyper/src/proto/h1/encode.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-hyper/src/proto/h1/httpunk.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-hyper/src/proto/h1/io.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-hyper/src/proto/h1/mod.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-hyper/src/proto/h1/role.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-hyper/src/proto/mod.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-hyper/src/trace.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-hyper-util/Cargo.toml +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-hyper-util/LICENSE +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-hyper-util/UPSTREAM_VERSION +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-hyper-util/src/client/mod.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-hyper-util/src/client/proxy/matcher.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-hyper-util/src/client/proxy/mod.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/crates/vendor-hyper-util/src/lib.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/httpunk/__init__.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/httpunk/_backend/__init__.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/httpunk/_common.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/httpunk/_httpunk.pyi +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/httpunk/asyncio.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/httpunk/h1/__init__.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/httpunk/h1/share.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/httpunk/h2/__init__.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/httpunk/h2/settings.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/httpunk/http.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/httpunk/py.typed +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/httpunk/types.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/httpunk/util/__init__.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/httpunk/util/auto.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/httpunk/util/client.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/httpunk/util/graceful.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/httpunk/util/proxy.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/src/errors.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/src/h1/codec.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/src/h1/mod.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/src/h2/codec.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/src/h2/mod.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/src/h2/streams.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/src/http/mod.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/src/lib.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/src/proxy.rs +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/tests/_client.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/tests/conftest.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/tests/test_asyncio_backend.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/tests/test_asyncio_client_protocol.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/tests/test_asyncio_protocol.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/tests/test_backend.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/tests/test_h1_codec.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/tests/test_h1_decode.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/tests/test_h2_codec.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/tests/test_h2_conn.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/tests/test_h2_flow_control.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/tests/test_h2_ready.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/tests/test_h2_settings.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/tests/test_h2_state.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/tests/test_headermap.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/tests/test_util_auto.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/tests/test_util_connect.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/tests/test_util_graceful.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/tests/test_util_pool.py +0 -0
- {httpunk-0.1.5 → httpunk-0.2.1}/tests/test_util_proxy.py +0 -0
|
@@ -55,9 +55,9 @@ checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
|
|
|
55
55
|
|
|
56
56
|
[[package]]
|
|
57
57
|
name = "h2"
|
|
58
|
-
version = "0.4.
|
|
58
|
+
version = "0.4.19"
|
|
59
59
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
60
|
-
checksum = "
|
|
60
|
+
checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16"
|
|
61
61
|
dependencies = [
|
|
62
62
|
"atomic-waker",
|
|
63
63
|
"bytes",
|
|
@@ -118,7 +118,7 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
|
|
118
118
|
|
|
119
119
|
[[package]]
|
|
120
120
|
name = "httpunk"
|
|
121
|
-
version = "0.1
|
|
121
|
+
version = "0.2.1"
|
|
122
122
|
dependencies = [
|
|
123
123
|
"bytes",
|
|
124
124
|
"h2",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "httpunk"
|
|
3
|
-
version = "0.1
|
|
3
|
+
version = "0.2.1"
|
|
4
4
|
description = "The Rust HTTP library for Python"
|
|
5
5
|
authors = [
|
|
6
6
|
"Giovanni Barillari <g@baro.dev>",
|
|
@@ -60,7 +60,7 @@ vendor-hyper-util = { path = "crates/vendor-hyper-util" }
|
|
|
60
60
|
# THIRD-PARTY.md). Optional + gated behind the never-enabled `_audit` feature so
|
|
61
61
|
# the pinned versions land in Cargo.lock for `cargo audit` / Dependabot advisory
|
|
62
62
|
# coverage without pulling them (and tokio) into the build.
|
|
63
|
-
h2 = { version = "=0.4.
|
|
63
|
+
h2 = { version = "=0.4.19", optional = true }
|
|
64
64
|
hyper = { version = "=1.11.0", optional = true, default-features = false, features = ["http1", "client", "server"] }
|
|
65
65
|
|
|
66
66
|
[profile.release]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: httpunk
|
|
3
|
-
Version: 0.1
|
|
4
|
-
Classifier: Development Status ::
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: BSD License
|
|
7
7
|
Classifier: Operating System :: MacOS
|
|
@@ -13,13 +13,14 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
16
17
|
Classifier: Programming Language :: Python :: Free Threading :: 3 - Stable
|
|
17
18
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
18
19
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
19
20
|
Classifier: Programming Language :: Python
|
|
20
21
|
Classifier: Programming Language :: Rust
|
|
21
22
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
22
|
-
Requires-Dist: tonio~=0.9.
|
|
23
|
+
Requires-Dist: tonio~=0.9.12 ; python_full_version >= '3.14' and extra == 'tonio'
|
|
23
24
|
Provides-Extra: tonio
|
|
24
25
|
License-File: LICENSE
|
|
25
26
|
Summary: The Rust HTTP library for Python
|
|
@@ -44,7 +45,7 @@ base for building HTTP clients and servers on top of.
|
|
|
44
45
|
|
|
45
46
|
httpunk's API mirrors hyper's wherever possible.
|
|
46
47
|
|
|
47
|
-
> **
|
|
48
|
+
> **Warning:** httpunk is in an early stage and still work in progress.
|
|
48
49
|
|
|
49
50
|
> **Note:** httpunk was built with substantial help from LLMs, under human supervision.
|
|
50
51
|
|
|
@@ -9,7 +9,7 @@ base for building HTTP clients and servers on top of.
|
|
|
9
9
|
|
|
10
10
|
httpunk's API mirrors hyper's wherever possible.
|
|
11
11
|
|
|
12
|
-
> **
|
|
12
|
+
> **Warning:** httpunk is in an early stage and still work in progress.
|
|
13
13
|
|
|
14
14
|
> **Note:** httpunk was built with substantial help from LLMs, under human supervision.
|
|
15
15
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.4.19
|
|
@@ -4,9 +4,12 @@ use super::{huffman, Header};
|
|
|
4
4
|
use bytes::{BufMut, BytesMut};
|
|
5
5
|
use http::header::{HeaderName, HeaderValue};
|
|
6
6
|
|
|
7
|
+
const DEFAULT_MAX_ALLOWED_SIZE: usize = 4 * 1024;
|
|
8
|
+
|
|
7
9
|
#[derive(Debug)]
|
|
8
10
|
pub struct Encoder {
|
|
9
11
|
table: Table,
|
|
12
|
+
max_allowed_size: usize,
|
|
10
13
|
size_update: Option<SizeUpdate>,
|
|
11
14
|
/// Reusable buffer for the encoded header block of a single frame.
|
|
12
15
|
///
|
|
@@ -25,13 +28,25 @@ enum SizeUpdate {
|
|
|
25
28
|
|
|
26
29
|
impl Encoder {
|
|
27
30
|
pub fn new(max_size: usize, capacity: usize) -> Encoder {
|
|
31
|
+
let max_size = max_size.min(DEFAULT_MAX_ALLOWED_SIZE);
|
|
32
|
+
|
|
28
33
|
Encoder {
|
|
29
34
|
table: Table::new(max_size, capacity),
|
|
35
|
+
max_allowed_size: DEFAULT_MAX_ALLOWED_SIZE,
|
|
30
36
|
size_update: None,
|
|
31
37
|
scratch: BytesMut::new(),
|
|
32
38
|
}
|
|
33
39
|
}
|
|
34
40
|
|
|
41
|
+
#[cfg(test)]
|
|
42
|
+
fn set_max_allowed_size(&mut self, max: usize) {
|
|
43
|
+
self.max_allowed_size = max;
|
|
44
|
+
|
|
45
|
+
if self.table.max_size() > max {
|
|
46
|
+
self.update_max_size(max);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
35
50
|
/// Takes the reusable buffer for one header-block encode.
|
|
36
51
|
///
|
|
37
52
|
/// The buffer is returned by `return_scratch` once the encoded block has
|
|
@@ -51,6 +66,8 @@ impl Encoder {
|
|
|
51
66
|
///
|
|
52
67
|
/// The next call to `encode` will include a dynamic size update frame.
|
|
53
68
|
pub fn update_max_size(&mut self, val: usize) {
|
|
69
|
+
let val = val.min(self.max_allowed_size);
|
|
70
|
+
|
|
54
71
|
match self.size_update {
|
|
55
72
|
Some(SizeUpdate::One(old)) => {
|
|
56
73
|
if val > old {
|
|
@@ -614,6 +631,7 @@ mod test {
|
|
|
614
631
|
assert_eq!(Some(SizeUpdate::Two(0, 100)), encoder.size_update);
|
|
615
632
|
|
|
616
633
|
let mut encoder = Encoder::default();
|
|
634
|
+
encoder.set_max_allowed_size(8000);
|
|
617
635
|
encoder.update_max_size(8000);
|
|
618
636
|
assert_eq!(Some(SizeUpdate::One(8000)), encoder.size_update);
|
|
619
637
|
|
|
@@ -701,6 +719,7 @@ mod test {
|
|
|
701
719
|
#[test]
|
|
702
720
|
fn test_large_size_update() {
|
|
703
721
|
let mut encoder = Encoder::default();
|
|
722
|
+
encoder.set_max_allowed_size(usize::MAX);
|
|
704
723
|
|
|
705
724
|
encoder.update_max_size(1912930560);
|
|
706
725
|
assert_eq!(Some(SizeUpdate::One(1912930560)), encoder.size_update);
|
|
@@ -710,6 +729,22 @@ mod test {
|
|
|
710
729
|
assert_eq!([63, 225, 129, 148, 144, 7], &dst[..]);
|
|
711
730
|
}
|
|
712
731
|
|
|
732
|
+
#[test]
|
|
733
|
+
fn test_large_size_update_is_capped() {
|
|
734
|
+
let mut encoder = Encoder::new(0, 0);
|
|
735
|
+
|
|
736
|
+
encoder.update_max_size(1912930560);
|
|
737
|
+
assert_eq!(
|
|
738
|
+
Some(SizeUpdate::One(DEFAULT_MAX_ALLOWED_SIZE)),
|
|
739
|
+
encoder.size_update
|
|
740
|
+
);
|
|
741
|
+
|
|
742
|
+
let mut dst = BytesMut::with_capacity(3);
|
|
743
|
+
encoder.encode_size_updates(&mut dst);
|
|
744
|
+
assert_eq!([63, 225, 31], &dst[..]);
|
|
745
|
+
assert_eq!(DEFAULT_MAX_ALLOWED_SIZE, encoder.table.max_size());
|
|
746
|
+
}
|
|
747
|
+
|
|
713
748
|
#[test]
|
|
714
749
|
#[ignore]
|
|
715
750
|
fn test_evicted_overflow() {
|
|
@@ -257,6 +257,17 @@ class AsyncioBackend:
|
|
|
257
257
|
drives the `close_notify`), so no plain/TLS split like tonio."""
|
|
258
258
|
transport.close()
|
|
259
259
|
|
|
260
|
+
def spawn_without_results(self, *coros):
|
|
261
|
+
"""Spawn task(s) NOW, discarding their results; returns the join handle
|
|
262
|
+
(see the tonio backend's twin for the full contract: await-once by a
|
|
263
|
+
single owner, no cancellation surface, coroutines route their own
|
|
264
|
+
errors). One coro -> its Task; several -> a gather with
|
|
265
|
+
`return_exceptions=True`, so the join waits for ALL of them exactly
|
|
266
|
+
like tonio's barrier does."""
|
|
267
|
+
if len(coros) == 1:
|
|
268
|
+
return asyncio.ensure_future(coros[0])
|
|
269
|
+
return asyncio.gather(*coros, return_exceptions=True)
|
|
270
|
+
|
|
260
271
|
async def select(self, *coros):
|
|
261
272
|
"""Race `coros`; return the first-ready one's result, cancelling the losers.
|
|
262
273
|
Matches tonio's `select` (`_ctl.select`: spawn all in argument order, keep the
|
|
@@ -294,6 +305,13 @@ class AsyncioBackend:
|
|
|
294
305
|
q = asyncio.Queue()
|
|
295
306
|
return _QueueSender(q), _QueueReceiver(q)
|
|
296
307
|
|
|
308
|
+
# Abrupt-peer-teardown exceptions (see the tonio backend's twin attribute):
|
|
309
|
+
# an RST surfaces as `ConnectionError` from `connection_lost`; a TLS close
|
|
310
|
+
# without close_notify as `ssl.SSLEOFError` (asyncio's SSL layer raises it
|
|
311
|
+
# on an unexpected EOF). The h1 server maps these at the request-head
|
|
312
|
+
# boundary to a clean end-of-iteration (F47).
|
|
313
|
+
broken_transport_errors = (ConnectionError, _ssl.SSLEOFError)
|
|
314
|
+
|
|
297
315
|
# asyncio's Lock/Event/Semaphore already match the seam's neutral contract
|
|
298
316
|
# (Event: set/wait/clear/is_set; Semaphore: async acquire / sync release — the
|
|
299
317
|
# shape step 1 normalized the h1 slot to).
|
|
@@ -13,7 +13,7 @@ from tonio.colored.net.tls import open_tls_over_tcp_stream as _open_tls_over_tcp
|
|
|
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
15
|
from tonio.colored.time import time as _now, timeout as _timeout
|
|
16
|
-
from tonio.exceptions import CancelledError
|
|
16
|
+
from tonio.exceptions import CancelledError, ResourceBroken
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class _TonioSemaphore:
|
|
@@ -106,6 +106,27 @@ class TonioBackend:
|
|
|
106
106
|
else:
|
|
107
107
|
transport.close()
|
|
108
108
|
|
|
109
|
+
# Exceptions `receive_some`/`send_all` raise when the peer tears the
|
|
110
|
+
# transport down ABRUPTLY instead of a clean EOF: an RST on plain TCP
|
|
111
|
+
# (`ConnectionError`: ConnectionResetError/BrokenPipeError), or a TLS close
|
|
112
|
+
# without close_notify (tonio wraps the SSLEOFError in `ResourceBroken`) —
|
|
113
|
+
# which httpunk's own sync `close_transport` produces by design (abortive
|
|
114
|
+
# close, F33a), so any httpunk client hanging up makes the peer see one.
|
|
115
|
+
# The h1 server maps these at the request-head boundary to a clean
|
|
116
|
+
# end-of-iteration (F47).
|
|
117
|
+
broken_transport_errors = (ConnectionError, ResourceBroken)
|
|
118
|
+
|
|
119
|
+
# Spawn task(s) NOW, discarding their results; returns the join handle.
|
|
120
|
+
# `await handle` resolves once ALL of them finished — exactly one owner
|
|
121
|
+
# awaits it, exactly once (tonio's handle is Barrier-based one-shot). NO
|
|
122
|
+
# cancellation surface, by design: this is the primitive for tasks that
|
|
123
|
+
# must never be cancelled (flush/handoff obligations — the h2 write pump,
|
|
124
|
+
# the h1 idle watcher); a group that needs teardown-with-cancel uses
|
|
125
|
+
# `scope`. Spawned coroutines must not let exceptions escape (drivers
|
|
126
|
+
# route errors into connection state); an escaped one surfaces at the
|
|
127
|
+
# join in a backend-specific shape and is a driver bug, not API.
|
|
128
|
+
spawn_without_results = staticmethod(_colored.spawn.without_results)
|
|
129
|
+
|
|
109
130
|
select = staticmethod(_colored.select)
|
|
110
131
|
scope = staticmethod(_colored.scope)
|
|
111
132
|
lock = _Lock
|
|
@@ -17,6 +17,15 @@ state-machine / flow-control / API-misuse errors raised by the extension share i
|
|
|
17
17
|
|
|
18
18
|
`error_code` attributes are `H2Reason` members for known codes (an `IntEnum`, so
|
|
19
19
|
they compare equal to ints), or a plain int for codes outside the RFC set.
|
|
20
|
+
|
|
21
|
+
An HTTP/1 `send_request` failure raised before the request was handed to the
|
|
22
|
+
writer carries `request_unsent = True` on the exception instance (whatever its
|
|
23
|
+
type — `ConnectionClosedError`, or the unexpected-bytes poison error): nothing
|
|
24
|
+
reached the wire, so the request is safe to retry with any body, streamed
|
|
25
|
+
included. The mirror of hyper's `TrySendError { message: Some(request) }`
|
|
26
|
+
give-back (client/conn/http1.rs L247-263; proto/h1/dispatch.rs L711-733). Read
|
|
27
|
+
it with `getattr(exc, "request_unsent", False)` — it is absent once the write
|
|
28
|
+
may have begun.
|
|
20
29
|
"""
|
|
21
30
|
|
|
22
31
|
from __future__ import annotations
|
|
@@ -12,6 +12,7 @@ Cross-reference: hyper `client::conn::http1` (`SendRequest`/`Connection`) +
|
|
|
12
12
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
|
+
import threading
|
|
15
16
|
from collections.abc import Awaitable
|
|
16
17
|
from typing import TYPE_CHECKING, Any
|
|
17
18
|
|
|
@@ -61,6 +62,12 @@ class Connection(H1ConnectionBase):
|
|
|
61
62
|
- `Expect: 100-continue` and the request's `Connection: close` are non-gaps for
|
|
62
63
|
the client: hyper hard-codes `expect_continue: false` (role.rs L1161) and
|
|
63
64
|
derives reuse solely from the response's keep-alive (conn.rs L294).
|
|
65
|
+
- While reusable-idle, a watcher task holds read interest — the re-expression
|
|
66
|
+
of hyper's always-polled `Connection` future (see `_watch_idle`) — so an
|
|
67
|
+
idle peer close or stray byte flips `closed`/`error` when it happens, and
|
|
68
|
+
a `send_request` failure raised before the request was handed to the
|
|
69
|
+
writer carries `request_unsent = True` (hyper's `TrySendError` message
|
|
70
|
+
give-back, client/conn/http1.rs L247-263).
|
|
64
71
|
"""
|
|
65
72
|
|
|
66
73
|
def __init__(self, transport, *, authority=None, backend=None):
|
|
@@ -87,22 +94,151 @@ class Connection(H1ConnectionBase):
|
|
|
87
94
|
# (a scope can't be re-armed after `cancel()`), so reuse isn't blocked.
|
|
88
95
|
self._writer_scope = None
|
|
89
96
|
self._writer_done = None
|
|
97
|
+
# The idle watcher: a background task parked in ONE read while the
|
|
98
|
+
# connection is reusable-idle — the Python re-expression of hyper's
|
|
99
|
+
# always-polled `Connection` future, which keeps read interest registered
|
|
100
|
+
# for the whole idle period (dispatch.rs L287 falls through to
|
|
101
|
+
# `poll_read_keep_alive`, conn.rs L435-443; `force_io_read` returns
|
|
102
|
+
# Pending, conn.rs L510-520), so a peer FIN or stray byte is observed
|
|
103
|
+
# when it ARRIVES, not at the next send. Python coroutines can't hold
|
|
104
|
+
# read interest without a parked task — the watcher is that task
|
|
105
|
+
# (divergence in mechanism only; behavior matches hyper, see
|
|
106
|
+
# `_watch_idle`). The watcher is NEVER cancelled or signalled: a parked
|
|
107
|
+
# `receive_some` legitimately ends only via data, EOF, or transport
|
|
108
|
+
# close (cancelling one leaves the socket's read registration armed —
|
|
109
|
+
# tonio contract), so its read completes naturally and is dispatched by
|
|
110
|
+
# `_exchange_active`: while idle it enforces the idle rules; once an
|
|
111
|
+
# exchange has started, the bytes that complete it ARE the response's
|
|
112
|
+
# first bytes, handed to `_read_head` via `_watcher_data`/`_watcher_error`
|
|
113
|
+
# — exactly hyper's structure, where one poll loop owns the read across
|
|
114
|
+
# the idle->busy transition. A bare task via `spawn_without_results`
|
|
115
|
+
# (the cancel-free seam primitive — a scope is for groups that DO get
|
|
116
|
+
# cancelled, like the body writer's): the handle IS the per-idle-period
|
|
117
|
+
# join, awaited by `_read_head` (handoff) or a teardown path.
|
|
118
|
+
self._watcher_handle = None
|
|
119
|
+
self._watcher_data = None
|
|
120
|
+
self._watcher_error = None
|
|
121
|
+
self._exchange_active = False
|
|
122
|
+
# Guards the take-ownership pop of the background-task handles
|
|
123
|
+
# (`_writer_scope`, `_watcher_handle`): a `close()` from another thread
|
|
124
|
+
# (tonio is work-stealing) can race the exchange's own join/teardown
|
|
125
|
+
# for the SAME handle — a second `Scope.__aexit__` raises RuntimeError
|
|
126
|
+
# (tonio `_exit`), spawn handles are await-once, and the handoff-field
|
|
127
|
+
# consumption must have one owner — the pop under the lock makes the
|
|
128
|
+
# loser see None. Held only for the sync swap, never across an await.
|
|
129
|
+
# Everything else the watcher shares is lock-free by construction: the
|
|
130
|
+
# handoff fields are published by the join, the handles/flag resets by
|
|
131
|
+
# the slot semaphore, and the `_exchange_active` read by the runtime's
|
|
132
|
+
# causal chain (flag-set -> head write -> peer response -> poller wake).
|
|
133
|
+
self._scopes_lock = threading.Lock()
|
|
90
134
|
|
|
91
135
|
async def connect(self):
|
|
92
|
-
|
|
136
|
+
# HTTP/1 has no connection preface / handshake (unlike h2), but hyper's
|
|
137
|
+
# `Connection` future is polled from the moment it is spawned — the
|
|
138
|
+
# dial-to-first-send window has read interest upstream, so it is watched
|
|
139
|
+
# here too.
|
|
140
|
+
self._start_watcher()
|
|
93
141
|
|
|
94
142
|
async def _teardown_writer(self, *, cancel):
|
|
95
143
|
"""Finish with the in-flight body writer: `cancel=True` aborts it (still
|
|
96
144
|
running — an early response we didn't wait out), then joins; `cancel=False`
|
|
97
|
-
just joins an already-finished writer (instant). Idempotent
|
|
98
|
-
|
|
145
|
+
just joins an already-finished writer (instant). Idempotent, and
|
|
146
|
+
single-owner via the pop under `_scopes_lock`: a `close()` from another
|
|
147
|
+
thread can race the exchange's own teardown for the SAME scope, and a
|
|
148
|
+
second `Scope.__aexit__` raises RuntimeError (tonio `_exit`) — the
|
|
149
|
+
losing popper sees None and returns instead."""
|
|
150
|
+
with self._scopes_lock:
|
|
151
|
+
scope, self._writer_scope, self._writer_done = self._writer_scope, None, None
|
|
99
152
|
if scope is None:
|
|
100
153
|
return
|
|
101
|
-
self._writer_scope = self._writer_done = None
|
|
102
154
|
if cancel:
|
|
103
155
|
scope.cancel()
|
|
104
156
|
await scope.__aexit__(None, None, None)
|
|
105
157
|
|
|
158
|
+
def _start_watcher(self):
|
|
159
|
+
"""Arm the idle watcher (see `_watch_idle`) for one idle period — SYNC:
|
|
160
|
+
the bare-task spawn suspends nothing, so callers have no interruption
|
|
161
|
+
window. They guarantee no exchange can be reading: `connect()` runs
|
|
162
|
+
before any request, and `release_slot` starts it BEFORE releasing the
|
|
163
|
+
slot — tonio is work-stealing, so a waiter resumed by the release can
|
|
164
|
+
run in parallel immediately, and the semaphore release/acquire is the
|
|
165
|
+
happens-before edge that guarantees it observes the watcher and takes
|
|
166
|
+
its read as the response's first read (F55 single-reader). No-op if the
|
|
167
|
+
connection can serve no more requests."""
|
|
168
|
+
if self._closed or self.error is not None or self.transport is None:
|
|
169
|
+
return
|
|
170
|
+
self._exchange_active = False
|
|
171
|
+
self._watcher_data = self._watcher_error = None
|
|
172
|
+
self._watcher_handle = self.backend.spawn_without_results(self._watch_idle())
|
|
173
|
+
|
|
174
|
+
async def _join_watcher(self):
|
|
175
|
+
"""JOIN the idle watcher (never cancel/signal it — see `_watch_idle`):
|
|
176
|
+
await its handle, which resolves when its single read has completed
|
|
177
|
+
and publishes the handoff fields. Idempotent; instant when none is
|
|
178
|
+
armed or it already finished. On teardown paths the transport is
|
|
179
|
+
closed first, which is what ends the parked read. The handle is taken
|
|
180
|
+
via a pop under `_scopes_lock` so a racing `close()` and exchange join
|
|
181
|
+
have one owner (handles are await-once); the loser sees None and
|
|
182
|
+
returns — the connection state it then observes is force-closed either
|
|
183
|
+
way."""
|
|
184
|
+
with self._scopes_lock:
|
|
185
|
+
handle, self._watcher_handle = self._watcher_handle, None
|
|
186
|
+
if handle is None:
|
|
187
|
+
return
|
|
188
|
+
await handle
|
|
189
|
+
|
|
190
|
+
async def _watch_idle(self):
|
|
191
|
+
"""ONE read, dispatched by connection state when it completes; the
|
|
192
|
+
spawn handle `_join_watcher` awaits resolves on exit. The
|
|
193
|
+
watcher is never cancelled or woken by a signal — a parked
|
|
194
|
+
`receive_some` legitimately ends only via data, EOF, or transport close
|
|
195
|
+
(cancellation would leave the socket's read registration armed for a
|
|
196
|
+
dead task, corrupting the next read). This is also hyper's structure:
|
|
197
|
+
the same poll loop owns the read across the idle->busy transition
|
|
198
|
+
(dispatch.rs L287 `poll_read` -> `poll_read_keep_alive` while idle, the
|
|
199
|
+
response read once busy), so there is no reader handoff to cancel.
|
|
200
|
+
|
|
201
|
+
Dispatch of the completed read (`_exchange_active` is set by
|
|
202
|
+
`send_request` before it writes):
|
|
203
|
+
|
|
204
|
+
- exchange active -> these are the response's first bytes (or `b""` =
|
|
205
|
+
EOF before the head, or a transport error): store them in
|
|
206
|
+
`_watcher_data`/`_watcher_error` for `_read_head`, touch nothing else
|
|
207
|
+
— the exchange's own paths handle errors exactly as if it had done
|
|
208
|
+
this read itself.
|
|
209
|
+
- idle EOF -> hyper's CLEAN idle close: "found EOF on idle connection,
|
|
210
|
+
closing" (conn.rs L471-481; `should_error_on_eof` is false when idle —
|
|
211
|
+
conn.rs L201-204). `_closed` only, NO error recorded: a pool discards
|
|
212
|
+
silently. The transport is closed NOW — hyper's connection task winds
|
|
213
|
+
down and drops its io (dispatch.rs L152-160) — so the socket never
|
|
214
|
+
parks in CLOSE_WAIT.
|
|
215
|
+
- idle bytes -> `new_unexpected_message` (conn.rs L484-489): poison +
|
|
216
|
+
close. Bytes racing `send_request`'s flag-set land in the same window
|
|
217
|
+
hyper has between `require_empty_read` and the head write — either
|
|
218
|
+
side of it, behavior matches upstream.
|
|
219
|
+
- idle transport error -> fail (hyper `force_io_read` io error ->
|
|
220
|
+
`state.close()`, conn.rs L510-520) — unless a concurrent `close()`
|
|
221
|
+
already committed the flags (its transport close is what woke us)."""
|
|
222
|
+
transport = self.transport
|
|
223
|
+
if transport is None: # a racing close() nulled it before we ran (F59)
|
|
224
|
+
return
|
|
225
|
+
try:
|
|
226
|
+
data = await transport.receive_some(65536)
|
|
227
|
+
except Exception as exc:
|
|
228
|
+
if self._exchange_active:
|
|
229
|
+
self._watcher_error = exc # surfaced by _read_head's first read
|
|
230
|
+
elif not self._closed:
|
|
231
|
+
self._fail(exc)
|
|
232
|
+
return
|
|
233
|
+
if self._exchange_active:
|
|
234
|
+
self._watcher_data = data # the response's first bytes (b"" = EOF)
|
|
235
|
+
return
|
|
236
|
+
if data:
|
|
237
|
+
self.poison_unexpected(len(data))
|
|
238
|
+
return
|
|
239
|
+
self._closed = True
|
|
240
|
+
self._close_transport()
|
|
241
|
+
|
|
106
242
|
def _acquire(self):
|
|
107
243
|
return self._slot.acquire() # blocks until the single in-flight slot is free
|
|
108
244
|
|
|
@@ -123,27 +259,47 @@ class Connection(H1ConnectionBase):
|
|
|
123
259
|
# modeled by the 1-permit slot.
|
|
124
260
|
await self._acquire()
|
|
125
261
|
self._busy = True
|
|
262
|
+
# The watcher may have observed EOF/bytes and closed/poisoned the
|
|
263
|
+
# connection while `_acquire` waited — the mirror of
|
|
264
|
+
# `SendRequest::poll_ready` failing once the connection task died
|
|
265
|
+
# (client/conn/http1.rs L156-180). `request_unsent` mirrors hyper
|
|
266
|
+
# handing the request back when the dispatch channel refuses it —
|
|
267
|
+
# `TrySendError { message: Some(req) }`, client/conn/http1.rs L247-263
|
|
268
|
+
# — and a queued-never-taken request canceled on a connection error
|
|
269
|
+
# (the idle-bytes poison, proto/h1/dispatch.rs L711-733): nothing was
|
|
270
|
+
# written, so the caller may safely retry ANY body, streamed included.
|
|
126
271
|
if self._closed or self.error is not None:
|
|
127
272
|
self._busy = False
|
|
128
273
|
self._slot.release()
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
#
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
#
|
|
274
|
+
exc = self.error or ConnectionClosedError("connection closed")
|
|
275
|
+
exc.request_unsent = True
|
|
276
|
+
raise exc
|
|
277
|
+
# `require_empty_read`'s buffered-bytes fast path at send time (conn.rs
|
|
278
|
+
# L463-465; regression test client_read_bytes_before_writing_request):
|
|
279
|
+
# a server may not send anything before the client's next request, and
|
|
280
|
+
# hyper's single poll loop serializes this check ahead of the write even
|
|
281
|
+
# under scheduler starvation — bytes ALREADY DELIVERED when a request is
|
|
282
|
+
# submitted are always a violation, never that request's "response". A
|
|
283
|
+
# parked watcher can't reproduce that ordering (its wake races this
|
|
284
|
+
# task), so the check runs here too, BEFORE `_exchange_active` is set —
|
|
285
|
+
# the ordering that makes it safe beside a parked watcher: if it steals
|
|
286
|
+
# the violation bytes, the watcher's spurious wake (empty read / closed
|
|
287
|
+
# transport) still dispatches through the idle rules against an
|
|
288
|
+
# already-poisoned connection and touches nothing (F31, problem b).
|
|
140
289
|
pending = self.backend.receive_nowait(self.transport, 65536)
|
|
141
290
|
if pending:
|
|
142
|
-
self.poison_unexpected(len(pending))
|
|
143
|
-
self._fail(self.error) # the stream is corrupt — close, don't reuse
|
|
291
|
+
self.poison_unexpected(len(pending)) # records the error AND closes
|
|
144
292
|
self._busy = False
|
|
145
293
|
self._slot.release()
|
|
294
|
+
self.error.request_unsent = True
|
|
146
295
|
raise self.error
|
|
296
|
+
# The exchange starts NOW: the watcher is not stopped (its parked read
|
|
297
|
+
# is never cancelled — see `_watch_idle`); this sync flag redirects its
|
|
298
|
+
# completing read from the idle rules to the exchange, whose `_read_head`
|
|
299
|
+
# joins it and takes the bytes as the response's first read. Bytes
|
|
300
|
+
# arriving from here to the head write are hyper's own race window
|
|
301
|
+
# (require_empty_read ran; the write turn follows).
|
|
302
|
+
self._exchange_active = True
|
|
147
303
|
try:
|
|
148
304
|
codec = H1Codec()
|
|
149
305
|
content_length, chunked = self._body_framing(body)
|
|
@@ -199,6 +355,12 @@ class Connection(H1ConnectionBase):
|
|
|
199
355
|
write_error = []
|
|
200
356
|
scope = self.backend.scope()
|
|
201
357
|
await scope.__aenter__()
|
|
358
|
+
# The `request_unsent` boundary: this spawn is httpunk's analogue of
|
|
359
|
+
# hyper's dispatcher taking the request off the channel (dispatch.rs
|
|
360
|
+
# `poll_msg`). From here on a failure NEVER carries the marker — in
|
|
361
|
+
# hyper, once taken, errors return `message: None` even if no byte
|
|
362
|
+
# reached the wire (an encoded-but-unflushed head is not handed
|
|
363
|
+
# back), and this boundary mirrors that exactly.
|
|
202
364
|
scope.spawn(self._write_request(codec, head, body, body_done, write_error, body_failed, trailers))
|
|
203
365
|
self._writer_scope, self._writer_done = scope, body_done
|
|
204
366
|
try:
|
|
@@ -223,6 +385,11 @@ class Connection(H1ConnectionBase):
|
|
|
223
385
|
# the caller owns; this driver won't touch the transport again
|
|
224
386
|
# (hyper `on_upgrade` / `Connection::into_parts`).
|
|
225
387
|
await self._teardown_writer(cancel=True) # the request-body write is moot
|
|
388
|
+
# No idle watcher can be running here: `_read_head` consumed it
|
|
389
|
+
# (its read delivered this 101 head's first bytes) and one is
|
|
390
|
+
# only re-armed by `release_slot` on a reuse verdict, which the
|
|
391
|
+
# upgrade path never reaches — a detached transport is never
|
|
392
|
+
# watched.
|
|
226
393
|
upgraded = H1Upgraded(self.transport, codec.take_body())
|
|
227
394
|
self._detach()
|
|
228
395
|
self._busy = False
|
|
@@ -247,6 +414,10 @@ class Connection(H1ConnectionBase):
|
|
|
247
414
|
self._fail(exc) # sync poison BEFORE the teardown suspension (see release_slot)
|
|
248
415
|
try:
|
|
249
416
|
await self._teardown_writer(cancel=True)
|
|
417
|
+
# A watcher not yet consumed by `_read_head` (failure before it
|
|
418
|
+
# ran): `_fail` closed the transport, which is what ends its
|
|
419
|
+
# parked read — join it (never cancel).
|
|
420
|
+
await self._join_watcher()
|
|
250
421
|
finally:
|
|
251
422
|
self._busy = False
|
|
252
423
|
self._slot.release()
|
|
@@ -283,8 +454,33 @@ class Connection(H1ConnectionBase):
|
|
|
283
454
|
# hyper: conn.rs `can_read_head` (L175) + `read_head` -> role.rs
|
|
284
455
|
# `Client::parse` (L1013), which loops past 1xx informational responses.
|
|
285
456
|
buffered = 0
|
|
457
|
+
data = None
|
|
458
|
+
if self._watcher_handle is not None:
|
|
459
|
+
# An idle watcher is armed: its parked `receive_some` is the
|
|
460
|
+
# connection's single reader (F55), so the response's FIRST read is
|
|
461
|
+
# its completing read — join it and take the handoff (bytes, `b""`
|
|
462
|
+
# EOF, or a transport error re-raised here, exactly as if this task
|
|
463
|
+
# had done the read). Only after the join does this task own the
|
|
464
|
+
# transport. Hyper needs no join: one poll loop does both reads.
|
|
465
|
+
await self._join_watcher()
|
|
466
|
+
exc, self._watcher_error = self._watcher_error, None
|
|
467
|
+
if exc is not None:
|
|
468
|
+
raise exc
|
|
469
|
+
data, self._watcher_data = self._watcher_data, None
|
|
470
|
+
if data is None:
|
|
471
|
+
# The watcher exited without a handoff: it dispatched through
|
|
472
|
+
# the IDLE rules against this exchange — either a racing
|
|
473
|
+
# close() tore the connection down under us, or bytes/EOF
|
|
474
|
+
# landed concurrently with the exchange start and the watcher
|
|
475
|
+
# saw the flag before this task's write of it was visible
|
|
476
|
+
# (free-threaded: the junk race has no causal ordering, both
|
|
477
|
+
# dispatches are legitimate). Surface the recorded verdict —
|
|
478
|
+
# the unexpected-bytes poison (hyper `new_unexpected_message`)
|
|
479
|
+
# or the clean close — not a generic error that would mask it.
|
|
480
|
+
raise self.error or ConnectionClosedError("connection closed")
|
|
286
481
|
while True:
|
|
287
|
-
data
|
|
482
|
+
if data is None:
|
|
483
|
+
data = await self.transport.receive_some(65536)
|
|
288
484
|
if not data:
|
|
289
485
|
# EOF before a full head. If the body write also failed (server
|
|
290
486
|
# closed both directions), surface that as the cause.
|
|
@@ -303,6 +499,7 @@ class Connection(H1ConnectionBase):
|
|
|
303
499
|
# just fails the connection (hyper `Parse::TooLarge`).
|
|
304
500
|
if buffered >= _MAX_HEAD_SIZE:
|
|
305
501
|
raise ValueError("response head too large (Parse::TooLarge)")
|
|
502
|
+
data = None
|
|
306
503
|
|
|
307
504
|
async def _await_body_failure(self, body_failed, write_error):
|
|
308
505
|
# Raced against `_read_head` (once per request, in send_request): if the request
|
|
@@ -315,13 +512,19 @@ class Connection(H1ConnectionBase):
|
|
|
315
512
|
raise write_error[0]
|
|
316
513
|
|
|
317
514
|
def poison_unexpected(self, nbytes):
|
|
318
|
-
"""The server sent `nbytes` unsolicited bytes
|
|
515
|
+
"""The server sent `nbytes` unsolicited bytes outside a response — an
|
|
319
516
|
HTTP/1 protocol violation (a server may not send anything before the next
|
|
320
|
-
request). hyper's client fails the connection
|
|
321
|
-
|
|
322
|
-
|
|
517
|
+
request). hyper's client fails the connection via `require_empty_read` ->
|
|
518
|
+
`new_unexpected_message` (conn.rs L463-465 buffered, L484-489 read) and
|
|
519
|
+
the errored connection task then winds down, dropping its io
|
|
520
|
+
(dispatch.rs `poll_catch` L123-141) — so record the error AND close, for
|
|
521
|
+
every caller (the idle watcher, the send-time guard, the past-body
|
|
522
|
+
leftover check in share.py). The next `send_request`/`wait_idle` raises
|
|
523
|
+
the recorded error."""
|
|
323
524
|
if self.error is None:
|
|
324
525
|
self.error = ValueError(f"received {nbytes} unexpected bytes on an idle HTTP/1 connection")
|
|
526
|
+
self._closed = True
|
|
527
|
+
self._close_transport()
|
|
325
528
|
|
|
326
529
|
async def release_slot(self, resp_keep_alive):
|
|
327
530
|
"""Free the in-flight slot once the caller has finished the response. Reuse
|
|
@@ -341,24 +544,38 @@ class Connection(H1ConnectionBase):
|
|
|
341
544
|
poisoned, and on reuse the exchange genuinely completed, so freeing the
|
|
342
545
|
slot is correct even if the writer join was cut short."""
|
|
343
546
|
fully_sent = self._writer_done is None or self._writer_done.is_set()
|
|
344
|
-
|
|
547
|
+
reuse = resp_keep_alive and fully_sent
|
|
548
|
+
if not reuse:
|
|
345
549
|
self._closed = True
|
|
346
550
|
self._close_transport() # sync by design (backend.close_transport)
|
|
347
551
|
try:
|
|
348
552
|
await self._teardown_writer(cancel=not fully_sent)
|
|
553
|
+
if reuse:
|
|
554
|
+
# The connection is reusable-idle again: restore hyper's idle
|
|
555
|
+
# read interest. Started BEFORE the slot release below (see
|
|
556
|
+
# `_start_watcher` — the semaphore is the visibility edge for a
|
|
557
|
+
# parallel waiter); `_start_watcher` no-ops if a racing
|
|
558
|
+
# close()/failure landed during the writer join.
|
|
559
|
+
self._start_watcher()
|
|
349
560
|
finally:
|
|
350
561
|
self._busy = False
|
|
351
562
|
self._slot.release()
|
|
352
563
|
|
|
353
564
|
async def close(self):
|
|
354
565
|
# Commit `_closed` + close the transport FIRST (the base close is sync —
|
|
355
|
-
# no suspension)
|
|
356
|
-
#
|
|
357
|
-
#
|
|
566
|
+
# no suspension): the transport close is what ends the idle watcher's
|
|
567
|
+
# parked read (it sees `_closed` and exits quietly) — then join it and
|
|
568
|
+
# abort + join a still-running background writer. The joins can be
|
|
569
|
+
# interrupted (they suspend); a force-close must never come away with
|
|
570
|
+
# `closed` still False.
|
|
358
571
|
await super().close()
|
|
572
|
+
await self._join_watcher()
|
|
359
573
|
await self._teardown_writer(cancel=True)
|
|
360
574
|
|
|
361
575
|
def _fail(self, exc):
|
|
576
|
+
# Sync poison + close. No live watcher to stop here: `_fail` runs only
|
|
577
|
+
# inside an exchange (slot held, watcher joined at send_request entry)
|
|
578
|
+
# or from the watcher itself.
|
|
362
579
|
if self.error is None and not isinstance(exc, ConnectionClosedError):
|
|
363
580
|
self.error = exc
|
|
364
581
|
self._closed = True
|
|
@@ -412,5 +629,12 @@ class H1Connection(BaseClientConnection):
|
|
|
412
629
|
absolute-form for a proxy, and an authority (``"host:port"``) is
|
|
413
630
|
authority-form for CONNECT. The caller supplies the ``Host`` header (we
|
|
414
631
|
never auto-add it), exactly like hyper's `client::conn::http1`.
|
|
632
|
+
|
|
633
|
+
A failure raised before the request was handed to the writer carries
|
|
634
|
+
``request_unsent = True`` on the exception: nothing reached the wire, so
|
|
635
|
+
the request — any body, streamed included — is safe to retry. The mirror
|
|
636
|
+
of hyper handing the request back in `TrySendError { message: Some }`
|
|
637
|
+
(client/conn/http1.rs L247-263; proto/h1/dispatch.rs L711-733). Absent
|
|
638
|
+
(falsy) once the write may have begun.
|
|
415
639
|
"""
|
|
416
640
|
return self._conn.send_request(request.method, request.target, request.headers, request.body, request.trailers)
|