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.
Files changed (133) hide show
  1. {httpunk-0.1.1 → httpunk-0.1.2}/Cargo.lock +1 -1
  2. {httpunk-0.1.1 → httpunk-0.1.2}/Cargo.toml +1 -1
  3. {httpunk-0.1.1 → httpunk-0.1.2}/PKG-INFO +8 -1
  4. {httpunk-0.1.1 → httpunk-0.1.2}/README.md +7 -0
  5. {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/_httpunk.pyi +5 -0
  6. {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/h1/server.py +30 -24
  7. {httpunk-0.1.1 → httpunk-0.1.2}/src/http/mod.rs +16 -0
  8. {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_h1_server.py +37 -0
  9. httpunk-0.1.2/tests/test_headermap.py +21 -0
  10. {httpunk-0.1.1 → httpunk-0.1.2}/LICENSE +0 -0
  11. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/Cargo.toml +0 -0
  12. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/LICENSE +0 -0
  13. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/UPSTREAM_VERSION +0 -0
  14. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/codec/error.rs +0 -0
  15. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/codec/mod.rs +0 -0
  16. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/ext.rs +0 -0
  17. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/frame/data.rs +0 -0
  18. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/frame/go_away.rs +0 -0
  19. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/frame/head.rs +0 -0
  20. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/frame/headers.rs +0 -0
  21. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/frame/mod.rs +0 -0
  22. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/frame/ping.rs +0 -0
  23. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/frame/priority.rs +0 -0
  24. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/frame/reason.rs +0 -0
  25. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/frame/reset.rs +0 -0
  26. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/frame/settings.rs +0 -0
  27. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/frame/stream_id.rs +0 -0
  28. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/frame/util.rs +0 -0
  29. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/frame/window_update.rs +0 -0
  30. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/hpack/decoder.rs +0 -0
  31. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/hpack/encoder.rs +0 -0
  32. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/hpack/header.rs +0 -0
  33. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/hpack/huffman/mod.rs +0 -0
  34. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/hpack/huffman/table.rs +0 -0
  35. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/hpack/mod.rs +0 -0
  36. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/hpack/table.rs +0 -0
  37. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/lib.rs +0 -0
  38. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/proto/error.rs +0 -0
  39. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/proto/mod.rs +0 -0
  40. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/proto/streams/flow_control.rs +0 -0
  41. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/proto/streams/mod.rs +0 -0
  42. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-h2/src/proto/streams/state.rs +0 -0
  43. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/Cargo.toml +0 -0
  44. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/LICENSE +0 -0
  45. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/UPSTREAM_VERSION +0 -0
  46. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/body/length.rs +0 -0
  47. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/body/mod.rs +0 -0
  48. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/cfg.rs +0 -0
  49. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/common/date.rs +0 -0
  50. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/common/mod.rs +0 -0
  51. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/error.rs +0 -0
  52. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/ext/h1_reason_phrase.rs +0 -0
  53. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/ext/informational.rs +0 -0
  54. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/ext/mod.rs +0 -0
  55. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/headers.rs +0 -0
  56. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/lib.rs +0 -0
  57. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/proto/h1/decode.rs +0 -0
  58. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/proto/h1/encode.rs +0 -0
  59. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/proto/h1/httpunk.rs +0 -0
  60. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/proto/h1/io.rs +0 -0
  61. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/proto/h1/mod.rs +0 -0
  62. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/proto/h1/role.rs +0 -0
  63. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/proto/mod.rs +0 -0
  64. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper/src/trace.rs +0 -0
  65. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper-util/Cargo.toml +0 -0
  66. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper-util/LICENSE +0 -0
  67. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper-util/UPSTREAM_VERSION +0 -0
  68. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper-util/src/client/mod.rs +0 -0
  69. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper-util/src/client/proxy/matcher.rs +0 -0
  70. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper-util/src/client/proxy/mod.rs +0 -0
  71. {httpunk-0.1.1 → httpunk-0.1.2}/crates/vendor-hyper-util/src/lib.rs +0 -0
  72. {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/__init__.py +0 -0
  73. {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/_backend/__init__.py +0 -0
  74. {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/_backend/asyncio.py +0 -0
  75. {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/_backend/tonio.py +0 -0
  76. {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/_common.py +0 -0
  77. {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/asyncio.py +0 -0
  78. {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/exceptions.py +0 -0
  79. {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/h1/__init__.py +0 -0
  80. {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/h1/client.py +0 -0
  81. {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/h1/connection.py +0 -0
  82. {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/h1/share.py +0 -0
  83. {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/h2/__init__.py +0 -0
  84. {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/h2/client.py +0 -0
  85. {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/h2/connection.py +0 -0
  86. {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/h2/server.py +0 -0
  87. {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/h2/settings.py +0 -0
  88. {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/h2/share.py +0 -0
  89. {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/h2/stream.py +0 -0
  90. {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/h2/streams.py +0 -0
  91. {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/http.py +0 -0
  92. {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/py.typed +0 -0
  93. {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/types.py +0 -0
  94. {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/util/__init__.py +0 -0
  95. {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/util/auto.py +0 -0
  96. {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/util/client.py +0 -0
  97. {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/util/graceful.py +0 -0
  98. {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/util/pool.py +0 -0
  99. {httpunk-0.1.1 → httpunk-0.1.2}/httpunk/util/proxy.py +0 -0
  100. {httpunk-0.1.1 → httpunk-0.1.2}/pyproject.toml +0 -0
  101. {httpunk-0.1.1 → httpunk-0.1.2}/src/errors.rs +0 -0
  102. {httpunk-0.1.1 → httpunk-0.1.2}/src/h1/codec.rs +0 -0
  103. {httpunk-0.1.1 → httpunk-0.1.2}/src/h1/mod.rs +0 -0
  104. {httpunk-0.1.1 → httpunk-0.1.2}/src/h2/codec.rs +0 -0
  105. {httpunk-0.1.1 → httpunk-0.1.2}/src/h2/mod.rs +0 -0
  106. {httpunk-0.1.1 → httpunk-0.1.2}/src/h2/streams.rs +0 -0
  107. {httpunk-0.1.1 → httpunk-0.1.2}/src/lib.rs +0 -0
  108. {httpunk-0.1.1 → httpunk-0.1.2}/src/proxy.rs +0 -0
  109. {httpunk-0.1.1 → httpunk-0.1.2}/tests/_client.py +0 -0
  110. {httpunk-0.1.1 → httpunk-0.1.2}/tests/conftest.py +0 -0
  111. {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_asyncio_backend.py +0 -0
  112. {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_asyncio_client_protocol.py +0 -0
  113. {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_asyncio_e2e.py +0 -0
  114. {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_asyncio_protocol.py +0 -0
  115. {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_backend.py +0 -0
  116. {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_h1_codec.py +0 -0
  117. {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_h1_conn.py +0 -0
  118. {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_h1_decode.py +0 -0
  119. {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_h2_codec.py +0 -0
  120. {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_h2_conn.py +0 -0
  121. {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_h2_errors.py +0 -0
  122. {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_h2_flow.py +0 -0
  123. {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_h2_flow_control.py +0 -0
  124. {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_h2_ready.py +0 -0
  125. {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_h2_server.py +0 -0
  126. {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_h2_settings.py +0 -0
  127. {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_h2_state.py +0 -0
  128. {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_util_auto.py +0 -0
  129. {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_util_connect.py +0 -0
  130. {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_util_graceful.py +0 -0
  131. {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_util_pool.py +0 -0
  132. {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_util_proxy.py +0 -0
  133. {httpunk-0.1.1 → httpunk-0.1.2}/tests/test_util_tls.py +0 -0
@@ -118,7 +118,7 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
118
118
 
119
119
  [[package]]
120
120
  name = "httpunk"
121
- version = "0.1.1"
121
+ version = "0.1.2"
122
122
  dependencies = [
123
123
  "bytes",
124
124
  "h2",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "httpunk"
3
- version = "0.1.1"
3
+ version = "0.1.2"
4
4
  description = "The Rust HTTP library for Python"
5
5
  authors = [
6
6
  "Giovanni Barillari <g@baro.dev>",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: httpunk
3
- Version: 0.1.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: race the read against a graceful-shutdown signal
376
- # so an idle keep-alive connection completes promptly (tonio can't
377
- # wake a parked recv from another task; hyper's poll re-checks
378
- # `should_read` and completes instead of reading).
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
- async def _read_or_shutdown(self, n):
407
- # Read the next head byte(s), or `_SHUTDOWN` if `graceful_shutdown()` fires
408
- # first. The backend's `select` cancels the losing branch, so the parked read
409
- # is cleanly dropped when shutdown wins.
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
- # asyncio: a plain read; if it parks, graceful_shutdown() wakes it via
415
- # `interrupt_read` (a shutdown-woken read returns b"") — no per-read select race.
416
- self._idle_read_parked = True
417
- try:
418
- data = await self.transport.receive_some(n)
419
- finally:
420
- self._idle_read_parked = False
421
- if not data and self._shutdown_evt.is_set():
422
- return _SHUTDOWN
423
- return data
424
-
425
- # Backends without native read control (tonio can't wake a parked recv from another
426
- # task): race the read against the shutdown signal via `select`.
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 await self.backend.select(_recv(), _await_shutdown())
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