httpunk 0.4.0__tar.gz → 0.4.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.4.0 → httpunk-0.4.1}/Cargo.lock +1 -1
- {httpunk-0.4.0 → httpunk-0.4.1}/Cargo.toml +1 -1
- {httpunk-0.4.0 → httpunk-0.4.1}/PKG-INFO +8 -2
- {httpunk-0.4.0 → httpunk-0.4.1}/README.md +6 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/httpunk/_backend/tonio.py +22 -12
- {httpunk-0.4.0 → httpunk-0.4.1}/httpunk/h1/server.py +68 -54
- {httpunk-0.4.0 → httpunk-0.4.1}/httpunk/h2/server.py +8 -2
- {httpunk-0.4.0 → httpunk-0.4.1}/pyproject.toml +1 -1
- {httpunk-0.4.0 → httpunk-0.4.1}/tests/test_backend.py +32 -10
- {httpunk-0.4.0 → httpunk-0.4.1}/tests/test_h1_server.py +25 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/tests/test_h2_server.py +3 -2
- {httpunk-0.4.0 → httpunk-0.4.1}/LICENSE +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/Cargo.toml +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/LICENSE +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/UPSTREAM_VERSION +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/src/codec/error.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/src/codec/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/src/error.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/src/ext.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/src/frame/data.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/src/frame/go_away.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/src/frame/head.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/src/frame/headers.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/src/frame/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/src/frame/ping.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/src/frame/priority.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/src/frame/reason.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/src/frame/reset.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/src/frame/settings.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/src/frame/stream_id.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/src/frame/util.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/src/frame/window_update.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/src/hpack/decoder.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/src/hpack/encoder.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/src/hpack/header.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/src/hpack/huffman/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/src/hpack/huffman/table.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/src/hpack/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/src/hpack/table.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/src/lib.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/src/proto/error.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/src/proto/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/src/proto/streams/flow_control.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/src/proto/streams/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-h2/src/proto/streams/state.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-hyper/Cargo.toml +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-hyper/LICENSE +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-hyper/UPSTREAM_VERSION +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-hyper/src/body/length.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-hyper/src/body/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-hyper/src/cfg.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-hyper/src/common/date.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-hyper/src/common/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-hyper/src/error.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-hyper/src/ext/h1_reason_phrase.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-hyper/src/ext/informational.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-hyper/src/ext/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-hyper/src/headers.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-hyper/src/lib.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-hyper/src/proto/h1/decode.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-hyper/src/proto/h1/encode.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-hyper/src/proto/h1/httpunk.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-hyper/src/proto/h1/io.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-hyper/src/proto/h1/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-hyper/src/proto/h1/role.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-hyper/src/proto/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-hyper/src/trace.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-hyper-util/Cargo.toml +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-hyper-util/LICENSE +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-hyper-util/UPSTREAM_VERSION +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-hyper-util/src/client/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-hyper-util/src/client/proxy/matcher.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-hyper-util/src/client/proxy/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/crates/vendor-hyper-util/src/lib.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/httpunk/__init__.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/httpunk/_backend/__init__.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/httpunk/_backend/asyncio.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/httpunk/_common.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/httpunk/_httpunk.pyi +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/httpunk/asyncio.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/httpunk/exceptions.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/httpunk/h1/__init__.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/httpunk/h1/client.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/httpunk/h1/connection.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/httpunk/h1/share.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/httpunk/h2/__init__.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/httpunk/h2/client.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/httpunk/h2/connection.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/httpunk/h2/share.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/httpunk/h2/stream.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/httpunk/http.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/httpunk/py.typed +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/httpunk/types.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/httpunk/util/__init__.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/httpunk/util/auto.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/httpunk/util/client.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/httpunk/util/graceful.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/httpunk/util/pool.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/httpunk/util/proxy.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/src/errors.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/src/h1/codec.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/src/h1/conn.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/src/h1/errors.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/src/h1/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/src/h2/codec.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/src/h2/conn.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/src/h2/errors.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/src/h2/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/src/h2/reason.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/src/h2/settings.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/src/h2/streams.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/src/http/mod.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/src/lib.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/src/proxy.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/src/py.rs +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/tests/_client.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/tests/_transport.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/tests/conftest.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/tests/test_asyncio_backend.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/tests/test_asyncio_client_protocol.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/tests/test_asyncio_e2e.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/tests/test_asyncio_protocol.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/tests/test_error_retention.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/tests/test_h1_codec.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/tests/test_h1_conn.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/tests/test_h1_decode.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/tests/test_h2_codec.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/tests/test_h2_conn.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/tests/test_h2_errors.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/tests/test_h2_flow.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/tests/test_h2_flow_control.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/tests/test_h2_ready.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/tests/test_h2_settings.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/tests/test_h2_state.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/tests/test_h2_write_batch.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/tests/test_headermap.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/tests/test_util_auto.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/tests/test_util_connect.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/tests/test_util_graceful.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/tests/test_util_pool.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/tests/test_util_proxy.py +0 -0
- {httpunk-0.4.0 → httpunk-0.4.1}/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.1
|
|
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
|
|
|
@@ -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]):
|
|
@@ -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:
|
|
@@ -215,34 +215,56 @@ async def _bounded(sock, timeout=1.0):
|
|
|
215
215
|
|
|
216
216
|
|
|
217
217
|
@pytest.mark.tonio
|
|
218
|
-
async def
|
|
219
|
-
|
|
218
|
+
async def test_receive_bounded_readable_at_once_never_arms():
|
|
219
|
+
# The syscall comes first (hyper `poll_read` before its timer poll): bytes there = done.
|
|
220
|
+
sock = _FakeArmSocket(arms=[], recvs=[b"head"])
|
|
220
221
|
assert await _bounded(sock) == b"head"
|
|
221
|
-
assert
|
|
222
|
+
assert sock.calls == [] and sock.cleared == 0 # no arm, no clock, no clear
|
|
222
223
|
|
|
223
224
|
|
|
224
225
|
@pytest.mark.tonio
|
|
225
226
|
async def test_receive_bounded_timer_wake_is_told_by_the_readiness_word():
|
|
226
|
-
#
|
|
227
|
-
|
|
227
|
+
# EAGAIN (the probe: stale bits, as tokio's readiness) -> clear -> arm with the remaining
|
|
228
|
+
# time -> park -> wake -> the readiness question hands out a waiter: no bits, the timer fired.
|
|
229
|
+
sock = _FakeArmSocket(arms=[True, True], recvs=[BlockingIOError])
|
|
228
230
|
assert await _bounded(sock, 0.5) is None
|
|
229
231
|
assert 499_000 < sock.calls[0] <= 500_000 # the arm carried the timer (micros; the clock ran a little)
|
|
230
232
|
assert sock.calls[1] is None # the question carries none
|
|
231
|
-
assert sock.cleared ==
|
|
233
|
+
assert sock.cleared == 1 # the one probe; no syscall after the timer wake
|
|
232
234
|
|
|
233
235
|
|
|
234
236
|
@pytest.mark.tonio
|
|
235
237
|
async def test_receive_bounded_readiness_wake_reads_bytes_first():
|
|
236
|
-
|
|
238
|
+
# EAGAIN -> clear -> arm -> park -> wake -> readable? yes -> the bytes.
|
|
239
|
+
sock = _FakeArmSocket(arms=[True, False], recvs=[BlockingIOError, b"head"])
|
|
237
240
|
assert await _bounded(sock) == b"head"
|
|
241
|
+
assert sock.cleared == 1
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@pytest.mark.tonio
|
|
245
|
+
async def test_receive_bounded_bits_landing_before_the_arm_read_at_once():
|
|
246
|
+
# EAGAIN -> clear -> arm finds the bits already set (data landed in between) -> read now.
|
|
247
|
+
sock = _FakeArmSocket(arms=[False], recvs=[BlockingIOError, b"head"])
|
|
248
|
+
assert await _bounded(sock) == b"head"
|
|
249
|
+
assert len(sock.calls) == 1 and sock.cleared == 1
|
|
238
250
|
|
|
239
251
|
|
|
240
252
|
@pytest.mark.tonio
|
|
241
253
|
async def test_receive_bounded_spurious_wake_rearms_with_the_remaining_time():
|
|
242
|
-
# park -> wake -> readable? yes -> EAGAIN (a spurious wake) -> clear -> re-arm, then bytes.
|
|
243
|
-
sock = _FakeArmSocket(arms=[True, False, True, False], recvs=[BlockingIOError, b"head"])
|
|
254
|
+
# ... -> park -> wake -> readable? yes -> EAGAIN (a spurious wake) -> clear -> re-arm, then bytes.
|
|
255
|
+
sock = _FakeArmSocket(arms=[True, False, True, False], recvs=[BlockingIOError, BlockingIOError, b"head"])
|
|
244
256
|
assert await _bounded(sock, 0.5) == b"head"
|
|
245
|
-
assert sock.cleared ==
|
|
257
|
+
assert sock.cleared == 2
|
|
246
258
|
first, again = sock.calls[0], sock.calls[2]
|
|
247
259
|
assert 499_000 < first <= 500_000
|
|
248
260
|
assert 0 < again <= first # the deadline is absolute: never reset to the full timeout
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@pytest.mark.tonio
|
|
264
|
+
async def test_receive_bounded_past_deadline_expires_without_arming():
|
|
265
|
+
# Nothing readable and the deadline already passed (a spurious wake landing after it, a
|
|
266
|
+
# partial head whose next read starts late): hyper's Pending + a ready timer poll ->
|
|
267
|
+
# `None` at once. No zero-length timer is ever armed.
|
|
268
|
+
sock = _FakeArmSocket(arms=[], recvs=[BlockingIOError])
|
|
269
|
+
assert await _bounded(sock, -0.001) is None
|
|
270
|
+
assert sock.calls == [] and sock.cleared == 1
|
|
@@ -1990,6 +1990,31 @@ async def test_server_header_read_timeout_bounds_the_watcher_hand_off():
|
|
|
1990
1990
|
await conn.close() # joins the watcher (its read ended by the close)
|
|
1991
1991
|
|
|
1992
1992
|
|
|
1993
|
+
@pytest.mark.tonio
|
|
1994
|
+
async def test_server_push_response_detect_eof_off_parks_nothing_until_asked():
|
|
1995
|
+
"""`send_response(detect_eof=False)` (httpunk's own knob): the head arms no
|
|
1996
|
+
mid-message watcher — no read parked, no task — and the body still goes out; a
|
|
1997
|
+
later `peer_closed()` arms on demand, exactly as it does before the head. The
|
|
1998
|
+
default keeps today's arm (`test_server_header_read_timeout_bounds_the_watcher_hand_off`)."""
|
|
1999
|
+
stub = _SilentStub(b"GET / HTTP/1.1\r\nhost: x\r\n\r\n")
|
|
2000
|
+
conn = ServerConnection(stub)
|
|
2001
|
+
await conn.start()
|
|
2002
|
+
req = await conn.next_request()
|
|
2003
|
+
stream = await req.send_response(200, headers={"content-length": "4"}, detect_eof=False)
|
|
2004
|
+
assert not conn.has_watcher and not stub.parked.is_set() # nothing parked by the head
|
|
2005
|
+
await stream.send_data(b"ti")
|
|
2006
|
+
assert not conn.has_watcher and not stub.parked.is_set() # nor mid-body
|
|
2007
|
+
seen = []
|
|
2008
|
+
async with scope() as s:
|
|
2009
|
+
s.spawn(_watch(req, seen)) # the explicit ask: arms now
|
|
2010
|
+
await stub.parked.wait()
|
|
2011
|
+
assert conn.has_watcher
|
|
2012
|
+
await stream.send_data(b"ck", end_stream=True) # the exchange completes: `False`
|
|
2013
|
+
assert seen == [False]
|
|
2014
|
+
assert stub.sent.endswith(b"tick")
|
|
2015
|
+
await conn.close() # joins the watcher (its read ended by the close)
|
|
2016
|
+
|
|
2017
|
+
|
|
1993
2018
|
@pytest.mark.asyncio
|
|
1994
2019
|
async def test_immediate_body_coalesces_with_the_head_up_to_the_codecs_buffer_cap():
|
|
1995
2020
|
"""hyper `WriteBuf` Flatten up to `max_buf_size`: an immediate body no larger than the
|
|
@@ -489,14 +489,15 @@ async def test_server_response_date_header_option():
|
|
|
489
489
|
@pytest.mark.tonio
|
|
490
490
|
async def test_server_push_response_streams_data_frames():
|
|
491
491
|
"""`send_response` + `send_data(..., end_stream=True)`: HEADERS (with Date) then DATA
|
|
492
|
-
frames, END_STREAM on the last; the connection keeps multiplexing afterwards.
|
|
492
|
+
frames, END_STREAM on the last; the connection keeps multiplexing afterwards.
|
|
493
|
+
`detect_eof` (the h1 twin's knob) is accepted and has no effect here."""
|
|
493
494
|
listener, host, port = await _listener()
|
|
494
495
|
|
|
495
496
|
async def serve():
|
|
496
497
|
transport = await listener.accept()
|
|
497
498
|
async with H2Server(transport) as server:
|
|
498
499
|
async for req in server:
|
|
499
|
-
stream = await req.send_response(200, headers={"x-path": req.path})
|
|
500
|
+
stream = await req.send_response(200, headers={"x-path": req.path}, detect_eof=req.path == "/2")
|
|
500
501
|
await stream.send_data(b"a" * 10)
|
|
501
502
|
await stream.send_data(b"b" * 5, end_stream=True)
|
|
502
503
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|