httpunk 0.1.1__tar.gz → 0.1.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.1.1 → httpunk-0.1.2}/Cargo.lock +1 -1
- {httpunk-0.1.1 → httpunk-0.1.2}/Cargo.toml +1 -1
- {httpunk-0.1.1 → httpunk-0.1.2}/PKG-INFO +8 -1
- {httpunk-0.1.1 → httpunk-0.1.2}/README.md +7 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/_httpunk.pyi +5 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/h1/server.py +30 -24
- {httpunk-0.1.1 → httpunk-0.1.2}/src/http/mod.rs +16 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_h1_server.py +37 -0
- httpunk-0.1.2/tests/test_headermap.py +21 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/LICENSE +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/Cargo.toml +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/LICENSE +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/UPSTREAM_VERSION +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/codec/error.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/codec/mod.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/ext.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/frame/data.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/frame/go_away.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/frame/head.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/frame/headers.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/frame/mod.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/frame/ping.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/frame/priority.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/frame/reason.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/frame/reset.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/frame/settings.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/frame/stream_id.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/frame/util.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/frame/window_update.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/hpack/decoder.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/hpack/encoder.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/hpack/header.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/hpack/huffman/mod.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/hpack/huffman/table.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/hpack/mod.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/hpack/table.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/lib.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/proto/error.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/proto/mod.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/proto/streams/flow_control.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/proto/streams/mod.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/proto/streams/state.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/Cargo.toml +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/LICENSE +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/UPSTREAM_VERSION +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/body/length.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/body/mod.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/cfg.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/common/date.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/common/mod.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/error.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/ext/h1_reason_phrase.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/ext/informational.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/ext/mod.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/headers.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/lib.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/proto/h1/decode.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/proto/h1/encode.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/proto/h1/httpunk.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/proto/h1/io.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/proto/h1/mod.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/proto/h1/role.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/proto/mod.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/trace.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper-util/Cargo.toml +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper-util/LICENSE +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper-util/UPSTREAM_VERSION +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper-util/src/client/mod.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper-util/src/client/proxy/matcher.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper-util/src/client/proxy/mod.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper-util/src/lib.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/__init__.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/_backend/__init__.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/_backend/asyncio.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/_backend/tonio.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/_common.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/asyncio.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/exceptions.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/h1/__init__.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/h1/client.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/h1/connection.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/h1/share.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/h2/__init__.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/h2/client.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/h2/connection.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/h2/server.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/h2/settings.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/h2/share.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/h2/stream.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/h2/streams.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/http.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/py.typed +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/types.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/util/__init__.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/util/auto.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/util/client.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/util/graceful.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/util/pool.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/util/proxy.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/pyproject.toml +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/src/errors.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/src/h1/codec.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/src/h1/mod.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/src/h2/codec.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/src/h2/mod.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/src/h2/streams.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/src/lib.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/src/proxy.rs +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/tests/_client.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/tests/conftest.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_asyncio_backend.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_asyncio_client_protocol.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_asyncio_e2e.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_asyncio_protocol.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_backend.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_h1_codec.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_h1_conn.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_h1_decode.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_h2_codec.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_h2_conn.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_h2_errors.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_h2_flow.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_h2_flow_control.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_h2_ready.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_h2_server.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_h2_settings.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_h2_state.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_util_auto.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_util_connect.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_util_graceful.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_util_pool.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_util_proxy.py +0 -0
- {httpunk-0.1.1 → httpunk-0.1.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.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: BSD License
|
|
@@ -265,6 +265,13 @@ h.get_all("set-cookie") # [b'a=1', b'b=2']
|
|
|
265
265
|
Anywhere a `headers=` argument is accepted you can pass a `HeaderMap`, a mapping, or an
|
|
266
266
|
iterable of `(name, value)` pairs.
|
|
267
267
|
|
|
268
|
+
For consumers that want raw pairs — e.g. an ASGI server building a scope's `headers` —
|
|
269
|
+
`raw_items()` returns `(bytes, bytes)` tuples (names already lowercase) in one call:
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
h.raw_items() # [(b'content-type', b'text/plain'), (b'set-cookie', b'a=1'), ...]
|
|
273
|
+
```
|
|
274
|
+
|
|
268
275
|
### Errors
|
|
269
276
|
|
|
270
277
|
httpunk's exceptions all derive from a common `HTTPunkError` root. `ConnectionClosedError`
|
|
@@ -230,6 +230,13 @@ h.get_all("set-cookie") # [b'a=1', b'b=2']
|
|
|
230
230
|
Anywhere a `headers=` argument is accepted you can pass a `HeaderMap`, a mapping, or an
|
|
231
231
|
iterable of `(name, value)` pairs.
|
|
232
232
|
|
|
233
|
+
For consumers that want raw pairs — e.g. an ASGI server building a scope's `headers` —
|
|
234
|
+
`raw_items()` returns `(bytes, bytes)` tuples (names already lowercase) in one call:
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
h.raw_items() # [(b'content-type', b'text/plain'), (b'set-cookie', b'a=1'), ...]
|
|
238
|
+
```
|
|
239
|
+
|
|
233
240
|
### Errors
|
|
234
241
|
|
|
235
242
|
httpunk's exceptions all derive from a common `HTTPunkError` root. `ConnectionClosedError`
|
|
@@ -88,6 +88,11 @@ class HeaderMap:
|
|
|
88
88
|
def items(self) -> list[tuple[str, bytes]]:
|
|
89
89
|
"""Every `(name, value)` pair, in order (duplicates included)."""
|
|
90
90
|
|
|
91
|
+
def raw_items(self) -> list[tuple[bytes, bytes]]:
|
|
92
|
+
"""Every `(name, value)` pair with the name as raw `bytes` (already lowercase
|
|
93
|
+
ASCII), in order, duplicates included — the exact shape ASGI servers want for a
|
|
94
|
+
scope's `headers`, in one boundary crossing with no per-name re-encoding."""
|
|
95
|
+
|
|
91
96
|
def __iter__(self) -> Iterator[str]: ...
|
|
92
97
|
def __len__(self) -> int: ...
|
|
93
98
|
def __eq__(self, other: object) -> bool: ...
|
|
@@ -372,13 +372,22 @@ class ServerConnection(H1ConnectionBase):
|
|
|
372
372
|
idle = not initial # no bytes of this request seen yet -> a graceful shutdown may end it
|
|
373
373
|
while True:
|
|
374
374
|
if idle:
|
|
375
|
-
# Between requests:
|
|
376
|
-
#
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
data = await self._read_or_shutdown(_READ_SIZE)
|
|
380
|
-
if data is _SHUTDOWN:
|
|
375
|
+
# Between requests: a graceful shutdown may end the wait (see
|
|
376
|
+
# `_read_or_shutdown` — sync; returns the sentinel or the read awaitable).
|
|
377
|
+
read = self._read_or_shutdown(_READ_SIZE)
|
|
378
|
+
if read is _SHUTDOWN:
|
|
381
379
|
return None
|
|
380
|
+
# Flag the park as idle so graceful_shutdown() may wake THIS read only —
|
|
381
|
+
# never a mid-head read (a request in flight must complete).
|
|
382
|
+
self._idle_read_parked = True
|
|
383
|
+
try:
|
|
384
|
+
data = await read
|
|
385
|
+
finally:
|
|
386
|
+
self._idle_read_parked = False
|
|
387
|
+
if data is _SHUTDOWN: # tonio select: the shutdown signal won the race
|
|
388
|
+
return None
|
|
389
|
+
# An interrupt_read shutdown wake surfaces as b"" and falls into the EOF
|
|
390
|
+
# return below — same outcome either way (loop ends; close, no response).
|
|
382
391
|
else:
|
|
383
392
|
data = await self.transport.receive_some(_READ_SIZE)
|
|
384
393
|
if not data:
|
|
@@ -403,27 +412,24 @@ class ServerConnection(H1ConnectionBase):
|
|
|
403
412
|
if buffered >= _MAX_HEAD_SIZE:
|
|
404
413
|
raise ValueError("message head is too large (Parse::TooLarge)")
|
|
405
414
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
415
|
+
def _read_or_shutdown(self, n):
|
|
416
|
+
"""The next idle-read awaitable, or `_SHUTDOWN` if a graceful shutdown was already
|
|
417
|
+
requested. A plain (sync) function so the fast path adds no wrapper coroutine: the
|
|
418
|
+
caller awaits the returned awaitable with `_idle_read_parked` set around it (so
|
|
419
|
+
`graceful_shutdown()` wakes only an idly-parked read, never a mid-head one) and
|
|
420
|
+
treats an awaited `_SHUTDOWN` — or a woken `b""` — as shutdown, not data.
|
|
421
|
+
|
|
422
|
+
asyncio (`native_read_interrupt`): a plain read; if it parks, graceful_shutdown()
|
|
423
|
+
wakes it via `interrupt_read` and the read returns b"". Backends without a native
|
|
424
|
+
read interrupt (tonio can't wake a parked recv from another task): race the read
|
|
425
|
+
against the shutdown signal via `select`, which cancels the losing branch — hyper's
|
|
426
|
+
poll instead re-checks `should_read` and completes without reading."""
|
|
410
427
|
if self._shutdown_evt.is_set():
|
|
411
428
|
return _SHUTDOWN
|
|
412
429
|
|
|
413
430
|
if self._native_read_interrupt:
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
self._idle_read_parked = True
|
|
417
|
-
try:
|
|
418
|
-
data = await self.transport.receive_some(n)
|
|
419
|
-
finally:
|
|
420
|
-
self._idle_read_parked = False
|
|
421
|
-
if not data and self._shutdown_evt.is_set():
|
|
422
|
-
return _SHUTDOWN
|
|
423
|
-
return data
|
|
424
|
-
|
|
425
|
-
# Backends without native read control (tonio can't wake a parked recv from another
|
|
426
|
-
# task): race the read against the shutdown signal via `select`.
|
|
431
|
+
return self.transport.receive_some(n)
|
|
432
|
+
|
|
427
433
|
def _recv():
|
|
428
434
|
return self.transport.receive_some(n)
|
|
429
435
|
|
|
@@ -431,7 +437,7 @@ class ServerConnection(H1ConnectionBase):
|
|
|
431
437
|
await self._shutdown_evt.wait()
|
|
432
438
|
return _SHUTDOWN
|
|
433
439
|
|
|
434
|
-
return
|
|
440
|
+
return self.backend.select(_recv(), _await_shutdown())
|
|
435
441
|
|
|
436
442
|
async def _send_error(self, codec, status):
|
|
437
443
|
"""Best-effort automatic error response (bodyless, `Connection: close`),
|
|
@@ -238,6 +238,22 @@ impl HeaderMap {
|
|
|
238
238
|
Ok(self.items_list(py)?.unbind())
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
+
/// Every `(name, value)` pair with the name as raw `bytes` (already lowercase
|
|
242
|
+
/// ASCII in the `http` crate), in order, duplicates included — the exact shape
|
|
243
|
+
/// ASGI servers want for a scope's `headers`, in one boundary crossing and with
|
|
244
|
+
/// no per-name re-encoding.
|
|
245
|
+
fn raw_items(&self, py: Python<'_>) -> PyResult<Py<PyList>> {
|
|
246
|
+
let guard = self.inner.lock().unwrap();
|
|
247
|
+
let list = PyList::empty(py);
|
|
248
|
+
for (name, value) in guard.iter() {
|
|
249
|
+
list.append((
|
|
250
|
+
PyBytes::new(py, name.as_str().as_bytes()),
|
|
251
|
+
PyBytes::new(py, value.as_bytes()),
|
|
252
|
+
))?;
|
|
253
|
+
}
|
|
254
|
+
Ok(list.unbind())
|
|
255
|
+
}
|
|
256
|
+
|
|
241
257
|
fn __iter__(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
|
242
258
|
let list = PyList::new(py, self.keys())?;
|
|
243
259
|
Ok(list.try_iter()?.into_any().unbind())
|
|
@@ -9,6 +9,7 @@ from _client import open_h1
|
|
|
9
9
|
from tonio.colored import scope
|
|
10
10
|
from tonio.colored.net import open_tcp_listeners
|
|
11
11
|
|
|
12
|
+
from httpunk._backend.asyncio import AsyncioBackend
|
|
12
13
|
from httpunk._backend.tonio import TonioBackend
|
|
13
14
|
from httpunk.exceptions import ConnectionClosedError
|
|
14
15
|
from httpunk.h1 import H1Server
|
|
@@ -77,6 +78,22 @@ class _StubTransport:
|
|
|
77
78
|
self.closed = True
|
|
78
79
|
|
|
79
80
|
|
|
81
|
+
class _PeekableStub(_StubTransport):
|
|
82
|
+
"""A stub whose SECOND payload becomes readable only after the first is consumed —
|
|
83
|
+
models a pipelined request sitting buffered in the transport while the previous
|
|
84
|
+
request is being served."""
|
|
85
|
+
|
|
86
|
+
def __init__(self, first, buffered):
|
|
87
|
+
super().__init__(first)
|
|
88
|
+
self._buffered = buffered
|
|
89
|
+
|
|
90
|
+
async def receive_some(self, max_bytes=65536):
|
|
91
|
+
if self._data:
|
|
92
|
+
return await super().receive_some(max_bytes)
|
|
93
|
+
chunk, self._buffered = self._buffered[:max_bytes], self._buffered[max_bytes:]
|
|
94
|
+
return chunk
|
|
95
|
+
|
|
96
|
+
|
|
80
97
|
async def _echo_server(listener, seen=None):
|
|
81
98
|
"""Accept one connection; echo each request as 200 `b"<METHOD> <target> -> " + body`."""
|
|
82
99
|
transport = await listener.accept()
|
|
@@ -276,6 +293,26 @@ async def test_server_header_read_timeout_closes_slow_head():
|
|
|
276
293
|
s.cancel()
|
|
277
294
|
|
|
278
295
|
|
|
296
|
+
@pytest.mark.asyncio
|
|
297
|
+
async def test_server_shutdown_wins_over_buffered_pipelined_request():
|
|
298
|
+
"""REGRESSION GUARD: a graceful-shutdown signal is honored BEFORE any idle read consumes
|
|
299
|
+
already-buffered bytes — hyper stops parsing new heads once keep-alive is disabled
|
|
300
|
+
(`can_read_head` under KA::Disabled), so a pipelined request already sitting in the
|
|
301
|
+
transport must NOT be served. Whitebox: the shutdown event is set directly (without
|
|
302
|
+
`graceful_shutdown()`'s `_reusable = False`, which would end the accept loop before the
|
|
303
|
+
read even starts) to pin the in-loop ordering: shutdown check -> read."""
|
|
304
|
+
transport = _PeekableStub(
|
|
305
|
+
b"GET /a HTTP/1.1\r\nhost: x\r\n\r\n",
|
|
306
|
+
b"GET /b HTTP/1.1\r\nhost: x\r\n\r\n",
|
|
307
|
+
)
|
|
308
|
+
conn = ServerConnection(transport, backend=AsyncioBackend())
|
|
309
|
+
req = await conn.next_request()
|
|
310
|
+
assert req.target == "/a"
|
|
311
|
+
await req.respond(200)
|
|
312
|
+
conn._shutdown_evt.set() # signal only (whitebox) — see docstring
|
|
313
|
+
assert await conn.next_request() is None # /b is readable but must NOT be parsed
|
|
314
|
+
|
|
315
|
+
|
|
279
316
|
@pytest.mark.tonio
|
|
280
317
|
async def test_server_detach_hands_off_open_connection():
|
|
281
318
|
"""detach() stops the accept loop and relinquishes the transport WITHOUT closing it,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""HeaderMap surface tests (Rust `http` crate wrapper) — currently just `raw_items`,
|
|
2
|
+
the ASGI-shaped `(bytes, bytes)` view; the rest of the surface is exercised throughout
|
|
3
|
+
the h1/h2 suites."""
|
|
4
|
+
|
|
5
|
+
from httpunk import HeaderMap
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_raw_items_matches_items_with_bytes_names():
|
|
9
|
+
h = HeaderMap({"Content-Type": "text/plain"})
|
|
10
|
+
h.add("set-cookie", "a=1")
|
|
11
|
+
h.add("Set-Cookie", "b=2") # names normalize to lowercase; duplicates kept in order
|
|
12
|
+
|
|
13
|
+
raw = h.raw_items()
|
|
14
|
+
assert raw == [(b"content-type", b"text/plain"), (b"set-cookie", b"a=1"), (b"set-cookie", b"b=2")]
|
|
15
|
+
# exact parity with items(), modulo the name type
|
|
16
|
+
assert raw == [(name.encode("latin-1"), value) for name, value in h.items()]
|
|
17
|
+
assert all(isinstance(n, bytes) and isinstance(v, bytes) for n, v in raw)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_raw_items_empty():
|
|
21
|
+
assert HeaderMap().raw_items() == []
|
|
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
|