httpunk 0.4.0__tar.gz → 0.4.2__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.4.0 → httpunk-0.4.2}/Cargo.lock +1 -1
- {httpunk-0.4.0 → httpunk-0.4.2}/Cargo.toml +1 -1
- {httpunk-0.4.0 → httpunk-0.4.2}/PKG-INFO +8 -2
- {httpunk-0.4.0 → httpunk-0.4.2}/README.md +6 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/httpunk/_backend/tonio.py +22 -12
- {httpunk-0.4.0 → httpunk-0.4.2}/httpunk/_httpunk.pyi +4 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/httpunk/h1/server.py +68 -54
- {httpunk-0.4.0 → httpunk-0.4.2}/httpunk/h2/client.py +32 -7
- {httpunk-0.4.0 → httpunk-0.4.2}/httpunk/h2/connection.py +56 -41
- {httpunk-0.4.0 → httpunk-0.4.2}/httpunk/h2/server.py +8 -2
- {httpunk-0.4.0 → httpunk-0.4.2}/pyproject.toml +1 -1
- {httpunk-0.4.0 → httpunk-0.4.2}/src/h2/conn.rs +355 -31
- {httpunk-0.4.0 → httpunk-0.4.2}/src/h2/mod.rs +1 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/tests/test_backend.py +32 -10
- {httpunk-0.4.0 → httpunk-0.4.2}/tests/test_h1_server.py +31 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/tests/test_h2_flow.py +147 -11
- httpunk-0.4.2/tests/test_h2_flow_control.py +384 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/tests/test_h2_server.py +13 -6
- {httpunk-0.4.0 → httpunk-0.4.2}/tests/test_util_pool.py +76 -70
- httpunk-0.4.0/tests/test_h2_flow_control.py +0 -174
- {httpunk-0.4.0 → httpunk-0.4.2}/LICENSE +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/Cargo.toml +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/LICENSE +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/UPSTREAM_VERSION +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/src/codec/error.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/src/codec/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/src/error.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/src/ext.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/src/frame/data.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/src/frame/go_away.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/src/frame/head.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/src/frame/headers.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/src/frame/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/src/frame/ping.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/src/frame/priority.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/src/frame/reason.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/src/frame/reset.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/src/frame/settings.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/src/frame/stream_id.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/src/frame/util.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/src/frame/window_update.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/src/hpack/decoder.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/src/hpack/encoder.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/src/hpack/header.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/src/hpack/huffman/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/src/hpack/huffman/table.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/src/hpack/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/src/hpack/table.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/src/lib.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/src/proto/error.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/src/proto/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/src/proto/streams/flow_control.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/src/proto/streams/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-h2/src/proto/streams/state.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-hyper/Cargo.toml +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-hyper/LICENSE +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-hyper/UPSTREAM_VERSION +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-hyper/src/body/length.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-hyper/src/body/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-hyper/src/cfg.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-hyper/src/common/date.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-hyper/src/common/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-hyper/src/error.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-hyper/src/ext/h1_reason_phrase.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-hyper/src/ext/informational.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-hyper/src/ext/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-hyper/src/headers.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-hyper/src/lib.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-hyper/src/proto/h1/decode.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-hyper/src/proto/h1/encode.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-hyper/src/proto/h1/httpunk.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-hyper/src/proto/h1/io.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-hyper/src/proto/h1/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-hyper/src/proto/h1/role.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-hyper/src/proto/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-hyper/src/trace.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-hyper-util/Cargo.toml +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-hyper-util/LICENSE +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-hyper-util/UPSTREAM_VERSION +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-hyper-util/src/client/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-hyper-util/src/client/proxy/matcher.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-hyper-util/src/client/proxy/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/crates/vendor-hyper-util/src/lib.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/httpunk/__init__.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/httpunk/_backend/__init__.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/httpunk/_backend/asyncio.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/httpunk/_common.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/httpunk/asyncio.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/httpunk/exceptions.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/httpunk/h1/__init__.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/httpunk/h1/client.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/httpunk/h1/connection.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/httpunk/h1/share.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/httpunk/h2/__init__.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/httpunk/h2/share.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/httpunk/h2/stream.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/httpunk/http.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/httpunk/py.typed +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/httpunk/types.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/httpunk/util/__init__.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/httpunk/util/auto.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/httpunk/util/client.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/httpunk/util/graceful.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/httpunk/util/pool.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/httpunk/util/proxy.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/src/errors.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/src/h1/codec.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/src/h1/conn.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/src/h1/errors.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/src/h1/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/src/h2/codec.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/src/h2/errors.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/src/h2/reason.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/src/h2/settings.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/src/h2/streams.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/src/http/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/src/lib.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/src/proxy.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/src/py.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/tests/_client.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/tests/_transport.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/tests/conftest.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/tests/test_asyncio_backend.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/tests/test_asyncio_client_protocol.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/tests/test_asyncio_e2e.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/tests/test_asyncio_protocol.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/tests/test_error_retention.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/tests/test_h1_codec.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/tests/test_h1_conn.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/tests/test_h1_decode.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/tests/test_h2_codec.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/tests/test_h2_conn.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/tests/test_h2_errors.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/tests/test_h2_ready.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/tests/test_h2_settings.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/tests/test_h2_state.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/tests/test_h2_write_batch.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/tests/test_headermap.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/tests/test_util_auto.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/tests/test_util_connect.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/tests/test_util_graceful.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/tests/test_util_proxy.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.2}/tests/test_util_tls.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: httpunk
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: BSD License
|
|
@@ -20,7 +20,7 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
|
20
20
|
Classifier: Programming Language :: Python
|
|
21
21
|
Classifier: Programming Language :: Rust
|
|
22
22
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
23
|
-
Requires-Dist: tonio~=0.9.
|
|
23
|
+
Requires-Dist: tonio~=0.9.17 ; python_full_version >= '3.14' and extra == 'tonio'
|
|
24
24
|
Provides-Extra: tonio
|
|
25
25
|
License-File: LICENSE
|
|
26
26
|
Summary: The Rust HTTP library for Python
|
|
@@ -275,6 +275,12 @@ await stream.send_data(b"data: 2\n\n", end_stream=True)
|
|
|
275
275
|
# stream.send_trailers({...}) ends the body with trailers; stream.send_reset() aborts it
|
|
276
276
|
```
|
|
277
277
|
|
|
278
|
+
On HTTP/1, `send_response` arms the mid-message detection described above (the body may
|
|
279
|
+
park between chunks); `detect_eof=False` skips that, so no read is parked for the response
|
|
280
|
+
and a client that closes is noticed one write later, at the peer's RST, instead — unless
|
|
281
|
+
`peer_closed()` asks, which still arms on demand. This is httpunk's own knob (hyper's read is
|
|
282
|
+
always on); HTTP/2 accepts it and ignores it, a reset arrives through the connection anyway.
|
|
283
|
+
|
|
278
284
|
`end_stream=True` on `send_response` is a bodyless response. Framing follows hyper: a
|
|
279
285
|
`content-length` you set is honoured, otherwise the body is chunked (HTTP/1.1) or
|
|
280
286
|
close-delimited (HTTP/1.0); on HEAD/204/304 body chunks are discarded, as hyper never polls
|
|
@@ -239,6 +239,12 @@ await stream.send_data(b"data: 2\n\n", end_stream=True)
|
|
|
239
239
|
# stream.send_trailers({...}) ends the body with trailers; stream.send_reset() aborts it
|
|
240
240
|
```
|
|
241
241
|
|
|
242
|
+
On HTTP/1, `send_response` arms the mid-message detection described above (the body may
|
|
243
|
+
park between chunks); `detect_eof=False` skips that, so no read is parked for the response
|
|
244
|
+
and a client that closes is noticed one write later, at the peer's RST, instead — unless
|
|
245
|
+
`peer_closed()` asks, which still arms on demand. This is httpunk's own knob (hyper's read is
|
|
246
|
+
always on); HTTP/2 accepts it and ignores it, a reset arrives through the connection anyway.
|
|
247
|
+
|
|
242
248
|
`end_stream=True` on `send_response` is a bodyless response. Framing follows hyper: a
|
|
243
249
|
`content-length` you set is honoured, otherwise the body is chunked (HTTP/1.1) or
|
|
244
250
|
close-delimited (HTTP/1.0); on HEAD/204/304 body chunks are discarded, as hyper never polls
|
|
@@ -145,10 +145,17 @@ class TonioBackend:
|
|
|
145
145
|
and the plain-socket reader is one coroutine over bound methods resolved here —
|
|
146
146
|
the cost of tonio's own `receive_some`, plus one arm on the (cold) timer path.
|
|
147
147
|
|
|
148
|
-
Bytes first, hyper's order (`poll_read_head`
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
148
|
+
Bytes first, hyper's order (`poll_read_head`: `parse` -> `poll_read`, and the timer
|
|
149
|
+
is polled only once that returned Pending): every pass tries the syscall before it
|
|
150
|
+
looks at the clock, and a wake that finds bytes returns them, whatever the timer did.
|
|
151
|
+
|
|
152
|
+
- **Plain socket**: the syscall comes first — the readiness bits are stale-set after
|
|
153
|
+
any successful read (nothing but a failed syscall clears them), exactly as tokio's
|
|
154
|
+
readiness word is when hyper's `poll_read` runs, so both pay one `EAGAIN` probe
|
|
155
|
+
per head; only on `EAGAIN` (`clear_r`, as tonio's own `recv`) is the remaining time
|
|
156
|
+
computed. Past the deadline that is the answer at once: hyper's Pending followed
|
|
157
|
+
by a ready timer poll (`new_header_timeout`) — no zero-length timer is ever
|
|
158
|
+
armed. Otherwise the socket's own arm (`_io_arm_r(timeout)`) puts the readiness
|
|
152
159
|
wait and the timer on ONE suspension (a `Waiter` with a timer). Both wakes resume
|
|
153
160
|
with `None`; the readiness word tells them apart: a readiness wake set bits
|
|
154
161
|
before waking (`set_readiness` precedes `wake`), a timer wake set none — so a
|
|
@@ -174,18 +181,21 @@ class TonioBackend:
|
|
|
174
181
|
|
|
175
182
|
async def read(max_bytes, deadline):
|
|
176
183
|
while True:
|
|
177
|
-
micros = round((deadline - _now()) * 1_000_000)
|
|
178
|
-
waiter = arm(0 if micros < 0 else micros)
|
|
179
|
-
if waiter is not None:
|
|
180
|
-
await waiter
|
|
181
|
-
if arm() is not None: # no readiness bits: the timer woke us
|
|
182
|
-
return None
|
|
183
184
|
try:
|
|
184
|
-
return recv(max_bytes) # bytes first
|
|
185
|
+
return recv(max_bytes) # bytes first: hyper's `poll_read` before its timer poll
|
|
185
186
|
except InterruptedError:
|
|
186
187
|
continue
|
|
187
188
|
except BlockingIOError:
|
|
188
|
-
clear() # a spurious
|
|
189
|
+
clear() # EAGAIN: the bits were stale (or a spurious wake) — now the timer
|
|
190
|
+
micros = round((deadline - _now()) * 1_000_000)
|
|
191
|
+
if micros <= 0:
|
|
192
|
+
return None # nothing readable and the deadline passed: hyper's timer poll is ready
|
|
193
|
+
waiter = arm(micros)
|
|
194
|
+
if waiter is None:
|
|
195
|
+
continue # bits landed between the EAGAIN and the arm: read now
|
|
196
|
+
await waiter
|
|
197
|
+
if arm() is not None: # no readiness bits: the timer woke us
|
|
198
|
+
return None
|
|
189
199
|
|
|
190
200
|
return read
|
|
191
201
|
|
|
@@ -867,6 +867,7 @@ H2_FLAG_WAKE: int # bytes were queued: wake the write pump
|
|
|
867
867
|
H2_FLAG_SLOT_FREED: int # client: a MAX_CONCURRENT slot freed / the limit changed
|
|
868
868
|
H2_FLAG_CONN_DONE: int # the connection failed or finished: wake the role waiters
|
|
869
869
|
H2_FLAG_STOP_ACCEPTING: int # server: the graceful drain completed
|
|
870
|
+
H2_FLAG_CAPACITY: int # the connection-capacity queue moved: ask `next_capacity_wake`
|
|
870
871
|
|
|
871
872
|
# `H2RecvHeadersVerdict.kind`.
|
|
872
873
|
H2_HEADERS_IGNORED: int
|
|
@@ -1060,6 +1061,9 @@ class H2Streams:
|
|
|
1060
1061
|
def stream_send_window(self, sid: int) -> int | None: ...
|
|
1061
1062
|
@property
|
|
1062
1063
|
def conn_send_window(self) -> int: ...
|
|
1064
|
+
def next_capacity_wake(self) -> object | None:
|
|
1065
|
+
"""After `H2_FLAG_CAPACITY`: the stream handle now first in line for connection
|
|
1066
|
+
window (wake its `window_evt`), or None."""
|
|
1063
1067
|
@property
|
|
1064
1068
|
def conn_recv_available(self) -> int: ...
|
|
1065
1069
|
@property
|
|
@@ -238,9 +238,9 @@ class ServerRequest:
|
|
|
238
238
|
self._conn.close_window(self._seq)
|
|
239
239
|
self._peer_closed_evt.set()
|
|
240
240
|
|
|
241
|
-
|
|
241
|
+
def respond(
|
|
242
242
|
self, status: int, *, headers: HeadersInput = None, body: Body = None, trailers: HeadersInput = None
|
|
243
|
-
) -> None:
|
|
243
|
+
) -> Awaitable[None]:
|
|
244
244
|
"""Send the whole response: head + body (+ trailers). `body` is None, `bytes`,
|
|
245
245
|
or a (sync/async) iterable of `bytes`. hyper: `Server::encode` + the dispatcher
|
|
246
246
|
polling the response `Body`. The pull convenience over `send_response`: same
|
|
@@ -258,9 +258,11 @@ class ServerRequest:
|
|
|
258
258
|
cancelled at that await (hyper drops the body future) and has unwound — its
|
|
259
259
|
cleanup ran — before this raises. Cancelling the task awaiting `respond()`
|
|
260
260
|
cancels the producer the same way."""
|
|
261
|
-
|
|
261
|
+
return self._conn._send_response(self, status, headers, body, trailers)
|
|
262
262
|
|
|
263
|
-
|
|
263
|
+
def send_response(
|
|
264
|
+
self, status: int, *, headers: HeadersInput = None, end_stream: bool = False, detect_eof: bool = True
|
|
265
|
+
) -> Awaitable[SendStream]:
|
|
264
266
|
"""Push-style response: send the head now and return a `SendStream` to write
|
|
265
267
|
the body with (`send_data` / `send_trailers` / `send_reset`). `end_stream=True`
|
|
266
268
|
= a bodyless response (hyper: a `Body` that `is_end_stream()` at encode time —
|
|
@@ -269,8 +271,18 @@ class ServerRequest:
|
|
|
269
271
|
`content-length`, which the encoder honours (hyper `set_length`'s
|
|
270
272
|
`existing_con_len`). hyper's h1 has no push API; this is the h1 twin of h2's
|
|
271
273
|
`SendResponse::send_response -> SendStream`, over the same codec calls
|
|
272
|
-
`respond()` makes, so the wire behaviour is hyper's either way.
|
|
273
|
-
|
|
274
|
+
`respond()` makes, so the wire behaviour is hyper's either way.
|
|
275
|
+
|
|
276
|
+
`detect_eof` (httpunk's own — hyper has no per-response knob, its mid-message read
|
|
277
|
+
is always on; the connection-level twin is `half_close`): a push body may park
|
|
278
|
+
between chunks, so by default this head arms the mid-message watcher (hyper
|
|
279
|
+
`mid_message_detect_eof`) so a client FIN mid-response is seen as it lands. `False`
|
|
280
|
+
skips that ask: nothing is parked for this response, saving the watcher task and
|
|
281
|
+
its hand-off at the next head read. A client that closes then is noticed one write
|
|
282
|
+
later — at the kernel's RST, not at the FIN — unless something asks: `peer_closed()`
|
|
283
|
+
still arms on demand, exactly as it does before the head. Meant for a caller whose
|
|
284
|
+
disconnect detection IS that explicit ask (an ASGI `receive()`)."""
|
|
285
|
+
return self._conn._send_response_head(self, status, headers, end_stream=end_stream, detect_eof=detect_eof)
|
|
274
286
|
|
|
275
287
|
def detach(self) -> bytes:
|
|
276
288
|
"""Take over the raw connection for a protocol upgrade (WebSocket, or a custom protocol):
|
|
@@ -323,7 +335,11 @@ class SendStream:
|
|
|
323
335
|
Fails with `ConnectionClosedError` once the client closed its side mid-request
|
|
324
336
|
(hyper: the connection errored with `IncompleteMessage`, nothing more is written)."""
|
|
325
337
|
self._check_open()
|
|
326
|
-
|
|
338
|
+
conn, req = self._conn, self._req
|
|
339
|
+
if conn.peer_closed_flag(req._seq):
|
|
340
|
+
# hyper: once `mid_message_detect_eof` saw EOF the connection is errored and
|
|
341
|
+
# nothing more is written for this exchange. Poison + close, `IncompleteMessage`.
|
|
342
|
+
await conn._fail_response(req, H1IncompleteMessageError(_PEER_CLOSED_MSG))
|
|
327
343
|
codec = self._codec
|
|
328
344
|
try:
|
|
329
345
|
if codec.body_is_eof():
|
|
@@ -336,28 +352,35 @@ class SendStream:
|
|
|
336
352
|
# `serialize_end` on a body short of its Content-Length: hyper `end_body` ->
|
|
337
353
|
# `User::BodyWriteAborted` + `Writing::Closed`. Same poison as a failed write.
|
|
338
354
|
self._done = True
|
|
339
|
-
await
|
|
355
|
+
await conn._fail_response(req, exc)
|
|
340
356
|
if end_stream:
|
|
341
357
|
self._done = True
|
|
342
358
|
if buf:
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
359
|
+
try:
|
|
360
|
+
await conn.write(buf)
|
|
361
|
+
except BaseException as exc:
|
|
362
|
+
self._done = True
|
|
363
|
+
await conn._fail_response(req, exc)
|
|
364
|
+
if end_stream and (end := conn._finish_response(req)) is not None:
|
|
365
|
+
await end
|
|
346
366
|
|
|
347
367
|
async def send_trailers(self, trailers: HeadersInput) -> None:
|
|
348
368
|
"""Finish a chunked body with a trailer block (hyper `Server::encode` allow-lists
|
|
349
369
|
the fields named in the response's `Trailer` header; others are dropped, and a
|
|
350
370
|
non-chunked body gets a bare terminator — `Encoder::encode_trailers`)."""
|
|
351
371
|
self._check_open()
|
|
352
|
-
|
|
372
|
+
conn, req = self._conn, self._req
|
|
373
|
+
if conn.peer_closed_flag(req._seq): # as `send_data`
|
|
374
|
+
await conn._fail_response(req, H1IncompleteMessageError(_PEER_CLOSED_MSG))
|
|
353
375
|
hdrs = trailers if isinstance(trailers, HeaderMap) else HeaderMap(trailers)
|
|
354
376
|
self._done = True
|
|
355
377
|
try:
|
|
356
378
|
buf = self._codec.serialize_trailers(hdrs) # bare terminator unless the request said `TE: trailers`
|
|
379
|
+
await conn.write(buf)
|
|
357
380
|
except BaseException as exc:
|
|
358
|
-
await
|
|
359
|
-
|
|
360
|
-
|
|
381
|
+
await conn._fail_response(req, exc)
|
|
382
|
+
if (end := conn._finish_response(req)) is not None:
|
|
383
|
+
await end
|
|
361
384
|
|
|
362
385
|
async def send_reset(self, reason: int | None = None) -> None:
|
|
363
386
|
"""Abort the response. HTTP/1 has no per-stream reset frame, so this is what
|
|
@@ -373,13 +396,6 @@ class SendStream:
|
|
|
373
396
|
if self._done:
|
|
374
397
|
raise RuntimeError("response body already complete")
|
|
375
398
|
|
|
376
|
-
async def _write(self, data):
|
|
377
|
-
try:
|
|
378
|
-
await self._conn.write(data)
|
|
379
|
-
except BaseException as exc:
|
|
380
|
-
self._done = True
|
|
381
|
-
await self._conn._fail_response(self._req, exc)
|
|
382
|
-
|
|
383
399
|
|
|
384
400
|
class ServerConnection(H1Framing, H1ServerState):
|
|
385
401
|
"""The server-side h1 driver: reads a request, hands back a `ServerRequest`,
|
|
@@ -602,12 +618,6 @@ class ServerConnection(H1Framing, H1ServerState):
|
|
|
602
618
|
return transport.receive_some(n)
|
|
603
619
|
return self._read_bounded(n, deadline)
|
|
604
620
|
|
|
605
|
-
async def _check_peer_open(self, req):
|
|
606
|
-
# hyper: once `mid_message_detect_eof` saw EOF the connection is errored and nothing
|
|
607
|
-
# more is written for this exchange. Poison + close, surface `IncompleteMessage`.
|
|
608
|
-
if self.peer_closed_flag(req._seq):
|
|
609
|
-
await self._fail_response(req, H1IncompleteMessageError(_PEER_CLOSED_MSG))
|
|
610
|
-
|
|
611
621
|
async def _send_async_body(self, req, body, trailers):
|
|
612
622
|
"""Stream an ASYNC response body and fail fast on a client FIN. hyper polls the
|
|
613
623
|
connection (so `mid_message_detect_eof`) while the body's next chunk is pending,
|
|
@@ -699,8 +709,16 @@ class ServerConnection(H1Framing, H1ServerState):
|
|
|
699
709
|
if code == H1_NEXT_NONE:
|
|
700
710
|
return None
|
|
701
711
|
codec = self._codec
|
|
712
|
+
# hyper's `header_read_timeout` (slowloris defence, http1.rs L249; default 30s, `None`
|
|
713
|
+
# disables it): ONE deadline per head, armed at the first poll for it — i.e. from the
|
|
714
|
+
# moment the connection is idle between requests, so it covers the keep-alive wait
|
|
715
|
+
# plus the head's bytes — never reset by partial bytes (conn.rs `poll_read_head`:
|
|
716
|
+
# `h1_header_read_timeout_running` stays set until a head parses). The bound rides
|
|
717
|
+
# each read (`_read_bounded`), so no task is spawned per head and no parked read is
|
|
718
|
+
# ever cancelled.
|
|
719
|
+
deadline = None if self._header_read_timeout is None else self._now() + self._header_read_timeout
|
|
702
720
|
try:
|
|
703
|
-
accepted = await self.
|
|
721
|
+
accepted = await self._read_head_frames(code, obj, deadline)
|
|
704
722
|
except H1ParseError:
|
|
705
723
|
# hyper conn.rs `on_parse_error`: an HTTP/2 preface is `Parse::VersionH2` and
|
|
706
724
|
# the connection errors (dropped: the abortive end); any other malformed head
|
|
@@ -722,6 +740,11 @@ class ServerConnection(H1Framing, H1ServerState):
|
|
|
722
740
|
# the clean EOF below, so it surfaces as a clean end-of-iteration (F47).
|
|
723
741
|
self._close(self.fail_read())
|
|
724
742
|
return None
|
|
743
|
+
if accepted is _DEADLINE:
|
|
744
|
+
# Expiry is `Error::new_header_timeout` out of the dispatcher: closed, no
|
|
745
|
+
# response, the abortive end.
|
|
746
|
+
self._close(self.fail_read())
|
|
747
|
+
return None
|
|
725
748
|
if accepted is None: # clean EOF between requests (or a shutdown released the idle read)
|
|
726
749
|
self.stop_serving()
|
|
727
750
|
return None
|
|
@@ -756,22 +779,6 @@ class ServerConnection(H1Framing, H1ServerState):
|
|
|
756
779
|
pass
|
|
757
780
|
return dec.is_complete
|
|
758
781
|
|
|
759
|
-
async def _read_request_head(self, code, obj):
|
|
760
|
-
# hyper's `header_read_timeout` (slowloris defence, http1.rs L249; default 30s, `None`
|
|
761
|
-
# disables it): ONE deadline per head, armed at the first poll for it — i.e. from the
|
|
762
|
-
# moment the connection is idle between requests, so it covers the keep-alive wait
|
|
763
|
-
# plus the head's bytes — never reset by partial bytes (conn.rs `poll_read_head`:
|
|
764
|
-
# `h1_header_read_timeout_running` stays set until a head parses). Expiry is
|
|
765
|
-
# `Error::new_header_timeout` out of the dispatcher: closed, no response, the
|
|
766
|
-
# abortive end. The bound rides each read (`_read_bounded`), so no task is spawned
|
|
767
|
-
# per head and no parked read is ever cancelled.
|
|
768
|
-
deadline = None if self._header_read_timeout is None else self._now() + self._header_read_timeout
|
|
769
|
-
accepted = await self._read_head_frames(code, obj, deadline)
|
|
770
|
-
if accepted is _DEADLINE:
|
|
771
|
-
self._close(self.fail_read())
|
|
772
|
-
return None
|
|
773
|
-
return accepted
|
|
774
|
-
|
|
775
782
|
async def _read_head_frames(self, code, obj, deadline):
|
|
776
783
|
"""Read + parse the next head from `begin_read`'s verdict: `READ_PARSE` = bytes of
|
|
777
784
|
it are already buffered (pipelined), parse before touching the transport;
|
|
@@ -888,17 +895,20 @@ class ServerConnection(H1Framing, H1ServerState):
|
|
|
888
895
|
await self._send_head_and_body(self._codec, head, body, trailers)
|
|
889
896
|
except BaseException as exc:
|
|
890
897
|
await self._fail_response(req, exc)
|
|
891
|
-
|
|
898
|
+
if (end := self._finish_response(req)) is not None:
|
|
899
|
+
await end
|
|
892
900
|
|
|
893
|
-
async def _send_response_head(self, req, status, headers, *, end_stream):
|
|
901
|
+
async def _send_response_head(self, req, status, headers, *, end_stream, detect_eof):
|
|
894
902
|
"""The push path (`ServerRequest.send_response`): write the head now, return
|
|
895
903
|
the `SendStream`. `end_stream` maps onto hyper's two encode inputs the same way a
|
|
896
904
|
pull body does: True = no body (`_body_framing(None)`), False = a body of unknown
|
|
897
905
|
length (streamed -> chunked, or length if the headers carry `content-length`).
|
|
898
906
|
The peer-closed check is the claim's (`_claim_response`), which precedes this."""
|
|
899
907
|
content_length, chunked = self._body_framing(None) if end_stream else (None, True)
|
|
900
|
-
# A push producer may park between chunks: make the FIN observable (`want`)
|
|
901
|
-
|
|
908
|
+
# A push producer may park between chunks: make the FIN observable (`want`) —
|
|
909
|
+
# unless the caller declined (`detect_eof=False`) or there is no body to park in.
|
|
910
|
+
want = detect_eof and not end_stream
|
|
911
|
+
head = await self._respond_head(req, status, headers, content_length, chunked, want=want)
|
|
902
912
|
stream = SendStream(self, req, self._codec)
|
|
903
913
|
if end_stream:
|
|
904
914
|
stream._done = True
|
|
@@ -908,8 +918,8 @@ class ServerConnection(H1Framing, H1ServerState):
|
|
|
908
918
|
await self.write(head)
|
|
909
919
|
except BaseException as exc:
|
|
910
920
|
await self._fail_response(req, exc)
|
|
911
|
-
if end_stream:
|
|
912
|
-
await
|
|
921
|
+
if end_stream and (end := self._finish_response(req)) is not None:
|
|
922
|
+
await end
|
|
913
923
|
return stream
|
|
914
924
|
|
|
915
925
|
async def _respond_head(self, req, status, headers, content_length, chunked, *, want):
|
|
@@ -990,12 +1000,15 @@ class ServerConnection(H1Framing, H1ServerState):
|
|
|
990
1000
|
self._close(self.fail_response(req._seq))
|
|
991
1001
|
req._peer_closed_evt.set()
|
|
992
1002
|
|
|
993
|
-
|
|
1003
|
+
def _finish_response(self, req):
|
|
994
1004
|
"""Completion, shared by both paths: the response is fully on the wire — apply
|
|
995
1005
|
the head-time reuse decision (hyper `try_keep_alive` after `Writing::KeepAlive`),
|
|
996
1006
|
or hand off the tunnel for a protocol switch. One step in the state; the
|
|
997
1007
|
`peer_closed()` waiters wake after it. A non-reusable connection completes:
|
|
998
|
-
`try_keep_alive` closes it and the future `poll_shutdown`s — the orderly end.
|
|
1008
|
+
`try_keep_alive` closes it and the future `poll_shutdown`s — the orderly end.
|
|
1009
|
+
Sync: the reusable case (the hot path) suspends nowhere; the close is handed back
|
|
1010
|
+
as the awaitable to finish with (None otherwise), so no wrapper coroutine rides
|
|
1011
|
+
every response."""
|
|
999
1012
|
switch, close, transport, leftover = self.finish_response(req._seq)
|
|
1000
1013
|
req._peer_closed_evt.set()
|
|
1001
1014
|
if switch:
|
|
@@ -1003,7 +1016,8 @@ class ServerConnection(H1Framing, H1ServerState):
|
|
|
1003
1016
|
# the start of the tunnel) is the caller's; the state detached.
|
|
1004
1017
|
req.upgraded = H1Upgraded(transport, leftover)
|
|
1005
1018
|
elif close:
|
|
1006
|
-
|
|
1019
|
+
return self._shutdown(transport)
|
|
1020
|
+
return None
|
|
1007
1021
|
|
|
1008
1022
|
|
|
1009
1023
|
class H1Server(BaseServer[ServerRequest]):
|
|
@@ -169,16 +169,41 @@ class Connection(H2ConnectionBase):
|
|
|
169
169
|
"""Send the request body concurrently with (not before) the caller awaiting
|
|
170
170
|
the response head: h2's `SendStream` (body) and `ResponseFuture` (head) are
|
|
171
171
|
independent — an early response that resets the request body (413 / redirect
|
|
172
|
-
during upload) must still deliver the received response.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
172
|
+
during upload) must still deliver the received response. hyper client.rs
|
|
173
|
+
`poll_pipe`: the pipe is polled once inline, and only if that leaves it pending
|
|
174
|
+
is it handed to the executor as a bare task that outlives `send_request` (full
|
|
175
|
+
duplex) and ends on its own — the body sent, or the stream stopped; `close()`
|
|
176
|
+
waits for the last one (`release_writers`), it never cancels them."""
|
|
177
|
+
offset = 0
|
|
178
|
+
if body is None or isinstance(body, bytes):
|
|
179
|
+
offset = self._send_body_inline(stream, body, trailers)
|
|
180
|
+
if offset is None:
|
|
181
|
+
return
|
|
182
|
+
if self.writer_start(): # refused: the stream is stopped already, nothing to write
|
|
183
|
+
self.backend.spawn_without_results(self._write_body(stream, body, trailers, offset))
|
|
184
|
+
|
|
185
|
+
def _send_body_inline(self, stream, body, trailers):
|
|
186
|
+
"""The inline first poll of a buffered body (hyper's `Full`: one chunk, EOS
|
|
187
|
+
set): queue what the windows take NOW. Returns the offset a background writer
|
|
188
|
+
resumes from, or None when nothing is left to write — the body (and trailers)
|
|
189
|
+
fully queued, or the stream already stopped, which the response reader sees."""
|
|
190
|
+
try:
|
|
191
|
+
offset = self._try_send_data(stream, body, 0, trailers is None) if body else None
|
|
192
|
+
if offset is None and trailers is not None:
|
|
193
|
+
self._send_trailers(stream, trailers)
|
|
194
|
+
except Exception:
|
|
195
|
+
return None
|
|
196
|
+
return offset
|
|
176
197
|
|
|
177
|
-
async def _write_body(self, stream, body, trailers
|
|
198
|
+
async def _write_body(self, stream, body, trailers, offset):
|
|
178
199
|
# The stream may error/reset mid-send: that is surfaced when the response body
|
|
179
200
|
# is read; a background writer has nowhere to propagate to.
|
|
180
|
-
|
|
181
|
-
|
|
201
|
+
try:
|
|
202
|
+
with contextlib.suppress(Exception):
|
|
203
|
+
await self._send_body(stream, body, trailers, offset)
|
|
204
|
+
finally:
|
|
205
|
+
if self.writer_end():
|
|
206
|
+
self._writers_evt.set()
|
|
182
207
|
|
|
183
208
|
|
|
184
209
|
class H2Connection(BaseClientConnection):
|
|
@@ -27,6 +27,7 @@ import contextlib
|
|
|
27
27
|
from .. import _backend
|
|
28
28
|
from .._common import PUMP_ABANDONED, PUMP_DONE, aclose_body, aiter_body, event_result
|
|
29
29
|
from .._httpunk import (
|
|
30
|
+
H2_FLAG_CAPACITY,
|
|
30
31
|
H2_FLAG_CONN_DONE,
|
|
31
32
|
H2_FLAG_SLOT_FREED,
|
|
32
33
|
H2_FLAG_STOP_ACCEPTING,
|
|
@@ -70,9 +71,12 @@ class H2ConnectionBase(H2Streams):
|
|
|
70
71
|
# pumps read it, `close()` closes it — never reassigned.
|
|
71
72
|
self.backend = _backend.resolve(backend)
|
|
72
73
|
self._transport = transport
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
|
|
74
|
+
# Set by the last background request-body writer to finish once `close()` is
|
|
75
|
+
# draining (client full-duplex send, F6): hyper's connection task ends only
|
|
76
|
+
# after every `PipeToSendStream` dropped its `conn_drop_ref`. Writers are bare
|
|
77
|
+
# tasks, never cancelled: a stopped stream (peer reset, GOAWAY, the connection
|
|
78
|
+
# failed or closed) is what ends one.
|
|
79
|
+
self._writers_evt = self.backend.event()
|
|
76
80
|
# Orders socket writes: the write pump and the inline flushes (`_flush`).
|
|
77
81
|
self._send_lock = self.backend.lock()
|
|
78
82
|
# Wakes the write pump. Set by the driver after any verdict that appended to
|
|
@@ -109,6 +113,12 @@ class H2ConnectionBase(H2Streams):
|
|
|
109
113
|
self._on_conn_done()
|
|
110
114
|
if flags & H2_FLAG_STOP_ACCEPTING:
|
|
111
115
|
self._on_stop_accepting()
|
|
116
|
+
if flags & H2_FLAG_CAPACITY:
|
|
117
|
+
# The front of the connection-capacity queue moved on: hand the turn
|
|
118
|
+
# to the next sender in line (h2 `assign_connection_capacity`).
|
|
119
|
+
handle = self.next_capacity_wake()
|
|
120
|
+
if handle is not None:
|
|
121
|
+
handle.window_evt.set()
|
|
112
122
|
|
|
113
123
|
@staticmethod
|
|
114
124
|
def _notify_stopped(handle, stop, notify_body):
|
|
@@ -187,7 +197,6 @@ class H2ConnectionBase(H2Streams):
|
|
|
187
197
|
# Shared handshake start (h2 client.rs/server.rs `handshake`): the preface
|
|
188
198
|
# (client) + our initial SETTINGS + the initial WINDOW_UPDATE(0) are queued by
|
|
189
199
|
# the state and flushed here, BEFORE the pumps start, so nothing can precede them.
|
|
190
|
-
await self._write_scope.__aenter__()
|
|
191
200
|
self.begin()
|
|
192
201
|
await self._flush()
|
|
193
202
|
# Bare tasks (`spawn_without_results`), never cancelled: every point they can
|
|
@@ -213,20 +222,21 @@ class H2ConnectionBase(H2Streams):
|
|
|
213
222
|
async def close(self):
|
|
214
223
|
# Stop the write pump by SIGNAL, flush-then-exit, and JOIN — never cancel it
|
|
215
224
|
# (a cancellation landing between its take and its flush would lose control
|
|
216
|
-
# frames).
|
|
217
|
-
#
|
|
218
|
-
#
|
|
219
|
-
#
|
|
220
|
-
#
|
|
221
|
-
#
|
|
222
|
-
#
|
|
225
|
+
# frames). Drain what is enqueued (h2 `codec.shutdown` = flush THEN shutdown,
|
|
226
|
+
# F23), shut the transport down — `FramedWrite::shutdown` ends in the IO's
|
|
227
|
+
# `poll_shutdown` (`close_notify` over TLS), which is also what ends the read
|
|
228
|
+
# pump's parked read — join it, and wake every straggler. On a transport that
|
|
229
|
+
# already died the alert cannot go out and the shutdown is the plain close
|
|
230
|
+
# hyper's drop would be: wire-identical. Last, wait for the background
|
|
231
|
+
# request-body writers: every stream is stopped by now, so each one wakes and
|
|
232
|
+
# exits — hyper's connection task likewise ends only after the last
|
|
233
|
+
# `PipeToSendStream` dropped its `conn_drop_ref`; the writers are never
|
|
234
|
+
# cancelled, here or there.
|
|
223
235
|
self.stop_pump()
|
|
224
236
|
self._write_evt.set()
|
|
225
237
|
handle = self.take_pump_handle()
|
|
226
238
|
if handle is not None:
|
|
227
239
|
await handle
|
|
228
|
-
self._write_scope.cancel()
|
|
229
|
-
await self._write_scope.__aexit__(None, None, None)
|
|
230
240
|
with contextlib.suppress(Exception): # best-effort: the connection is closing regardless
|
|
231
241
|
await self._flush()
|
|
232
242
|
await self.backend.shutdown_transport(self._transport)
|
|
@@ -234,6 +244,8 @@ class H2ConnectionBase(H2Streams):
|
|
|
234
244
|
if handle is not None:
|
|
235
245
|
await handle
|
|
236
246
|
self._fail(None) # no-op when the pump already failed everyone (F44)
|
|
247
|
+
if not self.release_writers():
|
|
248
|
+
await self._writers_evt.wait()
|
|
237
249
|
|
|
238
250
|
# ----- the write pump (h2 `poll_complete` draining its frame queue) -----
|
|
239
251
|
|
|
@@ -382,18 +394,19 @@ class H2ConnectionBase(H2Streams):
|
|
|
382
394
|
|
|
383
395
|
# ----- sending (h2 share.rs SendStream over send.rs, hyper PipeToSendStream) -----
|
|
384
396
|
|
|
385
|
-
async def _send_body(self, st, body, trailers=None):
|
|
397
|
+
async def _send_body(self, st, body, trailers=None, offset=0):
|
|
386
398
|
"""Stream a body, marking END_STREAM on the final DATA frame (or on the
|
|
387
399
|
trailing HEADERS), then close the send half. A bodyless message never reaches
|
|
388
|
-
here — its END_STREAM rode the HEADERS frame.
|
|
400
|
+
here — its END_STREAM rode the HEADERS frame. `offset`: where a bytes body
|
|
401
|
+
resumes after the client's inline attempt queued its head (`_send_body_inline`)."""
|
|
389
402
|
if isinstance(body, bytes):
|
|
390
403
|
# hyper's `Full` body: one poll yields the whole chunk with `is_end_stream()`
|
|
391
404
|
# set — one DATA carrying END_STREAM (or the trailers carry it, F45).
|
|
392
405
|
if trailers is None:
|
|
393
|
-
await self._send_data(st, body, end_stream=True)
|
|
406
|
+
await self._send_data(st, body, end_stream=True, offset=offset)
|
|
394
407
|
return
|
|
395
408
|
if body:
|
|
396
|
-
await self._send_data(st, body, end_stream=False)
|
|
409
|
+
await self._send_data(st, body, end_stream=False, offset=offset)
|
|
397
410
|
self._send_trailers(st, trailers)
|
|
398
411
|
return
|
|
399
412
|
if body is not None and not isinstance(body, bytearray) and hasattr(body, "__aiter__"):
|
|
@@ -459,16 +472,16 @@ class H2ConnectionBase(H2Streams):
|
|
|
459
472
|
await aclose_body(body)
|
|
460
473
|
done.set()
|
|
461
474
|
|
|
462
|
-
|
|
463
|
-
"""Queue `data`
|
|
464
|
-
stream window, send-buffer room, peer
|
|
465
|
-
behind `poll_capacity`. `send_data` is
|
|
466
|
-
append — and, with END_STREAM on the last
|
|
467
|
-
on the server the request's recv half
|
|
468
|
-
`RecvStream`: `handle` is the reader to notify
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
475
|
+
def _try_send_data(self, st, data, offset, end_stream):
|
|
476
|
+
"""Queue DATA frames from `data[offset:]` while the windows take them, each
|
|
477
|
+
reserved against min(connection window, stream window, send-buffer room, peer
|
|
478
|
+
max_frame_size) — h2 send.rs `send_data` behind `poll_capacity`. `send_data` is
|
|
479
|
+
one locked step (reserve + encode + append — and, with END_STREAM on the last
|
|
480
|
+
frame, the send half's close, as h2's; on the server the request's recv half
|
|
481
|
+
goes with it, hyper dropping the `RecvStream`: `handle` is the reader to notify
|
|
482
|
+
when its unread body was reset). Returns None once the last frame is queued, or
|
|
483
|
+
the offset to resume from on "no window now" (`sent == 0`, not `done`). Raises
|
|
484
|
+
the stopped error."""
|
|
472
485
|
while True:
|
|
473
486
|
v = self.send_data(st.id, data, offset, end_stream)
|
|
474
487
|
if v.stopped is not None:
|
|
@@ -478,23 +491,25 @@ class H2ConnectionBase(H2Streams):
|
|
|
478
491
|
if v.done:
|
|
479
492
|
if v.handle is not None:
|
|
480
493
|
self._notify_reset(v.handle, v.reader_stop)
|
|
481
|
-
return
|
|
494
|
+
return None
|
|
495
|
+
if not v.sent:
|
|
496
|
+
return offset
|
|
482
497
|
offset += v.sent
|
|
483
|
-
|
|
484
|
-
|
|
498
|
+
|
|
499
|
+
async def _send_data(self, st, data, end_stream, offset=0):
|
|
500
|
+
"""`_try_send_data` until the body is queued, awaiting "no window now" on
|
|
501
|
+
`window_evt` with the fixed waiter idiom: try, clear, try again, wait
|
|
502
|
+
(design §4)."""
|
|
503
|
+
while True:
|
|
504
|
+
offset = self._try_send_data(st, data, offset, end_stream)
|
|
505
|
+
if offset is None:
|
|
506
|
+
return
|
|
485
507
|
st.window_evt.clear()
|
|
486
|
-
|
|
487
|
-
if
|
|
488
|
-
raise self._stopped_error(st, v.stopped)
|
|
489
|
-
if v.flags:
|
|
490
|
-
self._after(v.flags)
|
|
491
|
-
if v.done:
|
|
492
|
-
if v.handle is not None:
|
|
493
|
-
self._notify_reset(v.handle, v.reader_stop)
|
|
508
|
+
resumed = self._try_send_data(st, data, offset, end_stream) # re-check after the clear
|
|
509
|
+
if resumed is None:
|
|
494
510
|
return
|
|
495
|
-
offset
|
|
496
|
-
|
|
497
|
-
await st.window_evt.wait()
|
|
511
|
+
offset = resumed
|
|
512
|
+
await st.window_evt.wait()
|
|
498
513
|
|
|
499
514
|
def _send_trailers(self, st, trailers):
|
|
500
515
|
"""The trailing HEADERS (END_STREAM) closes the send half in the same step
|
|
@@ -110,11 +110,17 @@ class ServerRequest:
|
|
|
110
110
|
cleanup ran — before this raises."""
|
|
111
111
|
return self._conn._send_response(self._stream, status, headers, body, trailers)
|
|
112
112
|
|
|
113
|
-
async def send_response(
|
|
113
|
+
async def send_response(
|
|
114
|
+
self, status: int, *, headers: HeadersInput = None, end_stream: bool = False, detect_eof: bool = True
|
|
115
|
+
) -> SendStream:
|
|
114
116
|
"""Push-style response: send HEADERS now and return a `SendStream` to write the
|
|
115
117
|
body with (`send_data` / `send_trailers` / `send_reset`). `end_stream=True` ends
|
|
116
118
|
the stream on the HEADERS frame (a bodyless response). h2:
|
|
117
|
-
`SendResponse::send_response(response, end_of_stream) -> SendStream`.
|
|
119
|
+
`SendResponse::send_response(response, end_of_stream) -> SendStream`.
|
|
120
|
+
|
|
121
|
+
`detect_eof` is accepted for API symmetry with the HTTP/1 twin and has no effect
|
|
122
|
+
here: a client abandoning the request is a RST_STREAM the connection's pump
|
|
123
|
+
delivers regardless (`reset_received`), there is no per-response read to arm."""
|
|
118
124
|
await self._conn._send_response_head(self._stream, status, headers, end_stream=end_stream)
|
|
119
125
|
stream = SendStream(self._conn, self._stream)
|
|
120
126
|
if end_stream:
|