httpunk 0.1.0a2__tar.gz → 0.1.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.
Files changed (132) hide show
  1. {httpunk-0.1.0a2 → httpunk-0.1.1}/Cargo.lock +11 -11
  2. {httpunk-0.1.0a2 → httpunk-0.1.1}/Cargo.toml +1 -1
  3. {httpunk-0.1.0a2 → httpunk-0.1.1}/PKG-INFO +2 -2
  4. {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/_backend/asyncio.py +21 -0
  5. {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/_backend/tonio.py +7 -2
  6. {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/h1/server.py +49 -15
  7. {httpunk-0.1.0a2 → httpunk-0.1.1}/pyproject.toml +1 -1
  8. {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_h1_server.py +29 -0
  9. {httpunk-0.1.0a2 → httpunk-0.1.1}/LICENSE +0 -0
  10. {httpunk-0.1.0a2 → httpunk-0.1.1}/README.md +0 -0
  11. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/Cargo.toml +0 -0
  12. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/LICENSE +0 -0
  13. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/UPSTREAM_VERSION +0 -0
  14. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/codec/error.rs +0 -0
  15. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/codec/mod.rs +0 -0
  16. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/ext.rs +0 -0
  17. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/frame/data.rs +0 -0
  18. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/frame/go_away.rs +0 -0
  19. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/frame/head.rs +0 -0
  20. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/frame/headers.rs +0 -0
  21. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/frame/mod.rs +0 -0
  22. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/frame/ping.rs +0 -0
  23. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/frame/priority.rs +0 -0
  24. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/frame/reason.rs +0 -0
  25. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/frame/reset.rs +0 -0
  26. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/frame/settings.rs +0 -0
  27. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/frame/stream_id.rs +0 -0
  28. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/frame/util.rs +0 -0
  29. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/frame/window_update.rs +0 -0
  30. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/hpack/decoder.rs +0 -0
  31. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/hpack/encoder.rs +0 -0
  32. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/hpack/header.rs +0 -0
  33. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/hpack/huffman/mod.rs +0 -0
  34. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/hpack/huffman/table.rs +0 -0
  35. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/hpack/mod.rs +0 -0
  36. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/hpack/table.rs +0 -0
  37. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/lib.rs +0 -0
  38. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/proto/error.rs +0 -0
  39. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/proto/mod.rs +0 -0
  40. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/proto/streams/flow_control.rs +0 -0
  41. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/proto/streams/mod.rs +0 -0
  42. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-h2/src/proto/streams/state.rs +0 -0
  43. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/Cargo.toml +0 -0
  44. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/LICENSE +0 -0
  45. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/UPSTREAM_VERSION +0 -0
  46. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/body/length.rs +0 -0
  47. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/body/mod.rs +0 -0
  48. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/cfg.rs +0 -0
  49. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/common/date.rs +0 -0
  50. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/common/mod.rs +0 -0
  51. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/error.rs +0 -0
  52. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/ext/h1_reason_phrase.rs +0 -0
  53. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/ext/informational.rs +0 -0
  54. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/ext/mod.rs +0 -0
  55. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/headers.rs +0 -0
  56. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/lib.rs +0 -0
  57. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/proto/h1/decode.rs +0 -0
  58. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/proto/h1/encode.rs +0 -0
  59. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/proto/h1/httpunk.rs +0 -0
  60. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/proto/h1/io.rs +0 -0
  61. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/proto/h1/mod.rs +0 -0
  62. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/proto/h1/role.rs +0 -0
  63. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/proto/mod.rs +0 -0
  64. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper/src/trace.rs +0 -0
  65. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper-util/Cargo.toml +0 -0
  66. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper-util/LICENSE +0 -0
  67. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper-util/UPSTREAM_VERSION +0 -0
  68. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper-util/src/client/mod.rs +0 -0
  69. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper-util/src/client/proxy/matcher.rs +0 -0
  70. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper-util/src/client/proxy/mod.rs +0 -0
  71. {httpunk-0.1.0a2 → httpunk-0.1.1}/crates/vendor-hyper-util/src/lib.rs +0 -0
  72. {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/__init__.py +0 -0
  73. {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/_backend/__init__.py +0 -0
  74. {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/_common.py +0 -0
  75. {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/_httpunk.pyi +0 -0
  76. {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/asyncio.py +0 -0
  77. {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/exceptions.py +0 -0
  78. {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/h1/__init__.py +0 -0
  79. {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/h1/client.py +0 -0
  80. {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/h1/connection.py +0 -0
  81. {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/h1/share.py +0 -0
  82. {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/h2/__init__.py +0 -0
  83. {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/h2/client.py +0 -0
  84. {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/h2/connection.py +0 -0
  85. {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/h2/server.py +0 -0
  86. {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/h2/settings.py +0 -0
  87. {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/h2/share.py +0 -0
  88. {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/h2/stream.py +0 -0
  89. {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/h2/streams.py +0 -0
  90. {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/http.py +0 -0
  91. {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/py.typed +0 -0
  92. {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/types.py +0 -0
  93. {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/util/__init__.py +0 -0
  94. {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/util/auto.py +0 -0
  95. {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/util/client.py +0 -0
  96. {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/util/graceful.py +0 -0
  97. {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/util/pool.py +0 -0
  98. {httpunk-0.1.0a2 → httpunk-0.1.1}/httpunk/util/proxy.py +0 -0
  99. {httpunk-0.1.0a2 → httpunk-0.1.1}/src/errors.rs +0 -0
  100. {httpunk-0.1.0a2 → httpunk-0.1.1}/src/h1/codec.rs +0 -0
  101. {httpunk-0.1.0a2 → httpunk-0.1.1}/src/h1/mod.rs +0 -0
  102. {httpunk-0.1.0a2 → httpunk-0.1.1}/src/h2/codec.rs +0 -0
  103. {httpunk-0.1.0a2 → httpunk-0.1.1}/src/h2/mod.rs +0 -0
  104. {httpunk-0.1.0a2 → httpunk-0.1.1}/src/h2/streams.rs +0 -0
  105. {httpunk-0.1.0a2 → httpunk-0.1.1}/src/http/mod.rs +0 -0
  106. {httpunk-0.1.0a2 → httpunk-0.1.1}/src/lib.rs +0 -0
  107. {httpunk-0.1.0a2 → httpunk-0.1.1}/src/proxy.rs +0 -0
  108. {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/_client.py +0 -0
  109. {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/conftest.py +0 -0
  110. {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_asyncio_backend.py +0 -0
  111. {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_asyncio_client_protocol.py +0 -0
  112. {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_asyncio_e2e.py +0 -0
  113. {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_asyncio_protocol.py +0 -0
  114. {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_backend.py +0 -0
  115. {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_h1_codec.py +0 -0
  116. {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_h1_conn.py +0 -0
  117. {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_h1_decode.py +0 -0
  118. {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_h2_codec.py +0 -0
  119. {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_h2_conn.py +0 -0
  120. {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_h2_errors.py +0 -0
  121. {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_h2_flow.py +0 -0
  122. {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_h2_flow_control.py +0 -0
  123. {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_h2_ready.py +0 -0
  124. {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_h2_server.py +0 -0
  125. {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_h2_settings.py +0 -0
  126. {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_h2_state.py +0 -0
  127. {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_util_auto.py +0 -0
  128. {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_util_connect.py +0 -0
  129. {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_util_graceful.py +0 -0
  130. {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_util_pool.py +0 -0
  131. {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_util_proxy.py +0 -0
  132. {httpunk-0.1.0a2 → httpunk-0.1.1}/tests/test_util_tls.py +0 -0
@@ -16,9 +16,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
16
16
 
17
17
  [[package]]
18
18
  name = "bytes"
19
- version = "1.11.1"
19
+ version = "1.12.1"
20
20
  source = "registry+https://github.com/rust-lang/crates.io-index"
21
- checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
21
+ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
22
22
 
23
23
  [[package]]
24
24
  name = "equivalent"
@@ -86,9 +86,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
86
86
 
87
87
  [[package]]
88
88
  name = "http"
89
- version = "1.4.0"
89
+ version = "1.4.2"
90
90
  source = "registry+https://github.com/rust-lang/crates.io-index"
91
- checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
91
+ checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
92
92
  dependencies = [
93
93
  "bytes",
94
94
  "itoa",
@@ -118,7 +118,7 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
118
118
 
119
119
  [[package]]
120
120
  name = "httpunk"
121
- version = "0.1.0-alpha2"
121
+ version = "0.1.1"
122
122
  dependencies = [
123
123
  "bytes",
124
124
  "h2",
@@ -175,9 +175,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
175
175
 
176
176
  [[package]]
177
177
  name = "libc"
178
- version = "0.2.183"
178
+ version = "0.2.186"
179
179
  source = "registry+https://github.com/rust-lang/crates.io-index"
180
- checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
180
+ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
181
181
 
182
182
  [[package]]
183
183
  name = "once_cell"
@@ -271,9 +271,9 @@ dependencies = [
271
271
 
272
272
  [[package]]
273
273
  name = "quote"
274
- version = "1.0.45"
274
+ version = "1.0.46"
275
275
  source = "registry+https://github.com/rust-lang/crates.io-index"
276
- checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
276
+ checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
277
277
  dependencies = [
278
278
  "proc-macro2",
279
279
  ]
@@ -292,9 +292,9 @@ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
292
292
 
293
293
  [[package]]
294
294
  name = "syn"
295
- version = "2.0.117"
295
+ version = "2.0.118"
296
296
  source = "registry+https://github.com/rust-lang/crates.io-index"
297
- checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
297
+ checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
298
298
  dependencies = [
299
299
  "proc-macro2",
300
300
  "quote",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "httpunk"
3
- version = "0.1.0-alpha2"
3
+ version = "0.1.1"
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.0a2
3
+ Version: 0.1.1
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: BSD License
@@ -19,7 +19,7 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
19
19
  Classifier: Programming Language :: Python
20
20
  Classifier: Programming Language :: Rust
21
21
  Classifier: Topic :: Internet :: WWW/HTTP
22
- Requires-Dist: tonio~=0.7.1 ; python_full_version >= '3.14' and extra == 'tonio'
22
+ Requires-Dist: tonio~=0.8.1 ; python_full_version >= '3.14' and extra == 'tonio'
23
23
  Provides-Extra: tonio
24
24
  License-File: LICENSE
25
25
  Summary: The Rust HTTP library for Python
@@ -162,6 +162,12 @@ class _AsyncioStream(asyncio.Protocol):
162
162
  if waiter is not None and not waiter.done():
163
163
  waiter.set_result(None)
164
164
 
165
+ def interrupt_read(self):
166
+ """Wake a parked `receive_some` so it returns b"" — lets a graceful shutdown release
167
+ an idle keep-alive read WITHOUT racing a coroutine (asyncio can wake a parked read from
168
+ another task; tonio can't, which is why it uses a select-based race instead)."""
169
+ self._wake_reader()
170
+
165
171
 
166
172
  class _AsyncioScope:
167
173
  """A nursery over `asyncio` tasks, matching tonio's scope surface: `spawn`,
@@ -297,3 +303,18 @@ class AsyncioBackend:
297
303
  scope = _AsyncioScope
298
304
  monotonic = staticmethod(_time.monotonic)
299
305
  sleep = staticmethod(asyncio.sleep) # async sleep(seconds), for deadline races (`select`)
306
+
307
+ # Every backend implements `timeout` (above), so the h1 server always uses it for the
308
+ # head-read deadline. This extra capability is asyncio-only: `_AsyncioStream.interrupt_read`
309
+ # can wake a parked idle read from another task, so the h1 server can skip the per-read
310
+ # shutdown `select`. tonio can't (no such method) → it keeps the select there.
311
+ native_read_interrupt = True
312
+
313
+ async def timeout(self, coro, seconds):
314
+ """Run `coro` bounded by `seconds`; return `(result, True)` on completion, or
315
+ `(None, False)` on timeout (the coro is cancelled). One task + one timer, far cheaper
316
+ than racing a `sleep()` coroutine via `select`."""
317
+ try:
318
+ return await asyncio.wait_for(coro, seconds), True
319
+ except (TimeoutError, asyncio.TimeoutError):
320
+ return None, False
@@ -12,7 +12,7 @@ from tonio.colored.net import open_tcp_stream as _open_tcp_stream
12
12
  from tonio.colored.net.tls import open_tls_over_tcp_stream as _open_tls_over_tcp_stream
13
13
  from tonio.colored.sync import Lock as _Lock, Semaphore as _Semaphore
14
14
  from tonio.colored.sync.channel import unbounded as _unbounded
15
- from tonio.colored.time import time as _now
15
+ from tonio.colored.time import time as _now, timeout as _timeout
16
16
 
17
17
 
18
18
  class _TonioSemaphore:
@@ -107,4 +107,9 @@ class TonioBackend:
107
107
  semaphore = _TonioSemaphore
108
108
  queue = staticmethod(_unbounded)
109
109
  monotonic = staticmethod(_now)
110
- sleep = staticmethod(_colored.sleep) # async sleep(seconds), for deadline races (`select`)
110
+ sleep = staticmethod(_colored.sleep) # async sleep(seconds)
111
+ # `timeout(coro, seconds) -> (result, completed)` — tonio's native deadline; the h1 server
112
+ # uses it to bound the request-head read (measured ~12-16% faster than racing sleep() via
113
+ # select). Same contract as AsyncioBackend.timeout. tonio still can't wake a parked recv from
114
+ # another task, so it does NOT advertise `native_read_interrupt` (idle shutdown stays select).
115
+ timeout = staticmethod(_timeout)
@@ -53,7 +53,6 @@ _CONTINUE = b"HTTP/1.1 100 Continue\r\n\r\n"
53
53
  # (io.rs: 8192 + 4096*100). Bounds per-connection memory against a slow/oversized head.
54
54
  _MAX_HEAD_SIZE = 8192 + 4096 * 100
55
55
  _DEFAULT_HEADER_READ_TIMEOUT = 30.0 # hyper's `header_read_timeout` default (http1.rs L249)
56
- _TIMED_OUT = object() # sentinel: the header read exceeded `header_read_timeout`
57
56
  # The HTTP/2 connection preface (client prior-knowledge). An h1 server that sees it
58
57
  # closes silently with a version error rather than writing a 400 (hyper `on_parse_error`
59
58
  # -> `has_h2_prefix` -> `new_version_h2`, conn.rs L29/L809-812).
@@ -174,6 +173,22 @@ class ServerRequest:
174
173
  self._responded = True
175
174
  await self._conn.send_response(self, status, headers, body)
176
175
 
176
+ def detach(self) -> bytes:
177
+ """Take over the raw connection for a protocol upgrade (WebSocket, or a custom protocol):
178
+ stop the server's accept loop and relinquish the transport WITHOUT closing it, returning
179
+ any bytes already read past the request head (to replay). The caller — which supplied the
180
+ transport to `H1Server` — owns it afterwards and must drive the upgrade itself (httpunk
181
+ sends no response). Like httpunk's 101/CONNECT tunnel hand-off, but caller-driven.
182
+
183
+ After this, the accept loop ends (`next_request` returns None) and the server's close on
184
+ `__aexit__` is a no-op, so the transport stays open. cf. Go's `http.Hijacker`."""
185
+ if self._responded:
186
+ raise RuntimeError("cannot detach: a response was already sent for this request")
187
+ self._responded = True
188
+ leftover = self._decoder.take_buffered()
189
+ self._conn._detach()
190
+ return leftover
191
+
177
192
  def __repr__(self) -> str:
178
193
  return f"ServerRequest(method={self.method!r}, target={self.target!r})"
179
194
 
@@ -193,6 +208,12 @@ class ServerConnection(H1ConnectionBase):
193
208
  # Max time to read a complete request head before closing (slowloris defence),
194
209
  # hyper's `header_read_timeout` (default 30s, http1.rs L249). `None` disables it.
195
210
  self._header_read_timeout = header_read_timeout
211
+ # The head-read deadline always uses `backend.timeout` (every backend implements it).
212
+ # This capability is extra: a backend that can wake a parked read from another task
213
+ # (asyncio) lets us skip the per-read shutdown `select`; absent (tonio) → use `select`.
214
+ # See `_read_or_shutdown` + `graceful_shutdown`.
215
+ self._native_read_interrupt = getattr(self.backend, "native_read_interrupt", False)
216
+ self._idle_read_parked = False # True only while parked in the idle between-requests read
196
217
 
197
218
  async def start(self):
198
219
  pass # HTTP/1 has no connection preface / handshake
@@ -206,6 +227,10 @@ class ServerConnection(H1ConnectionBase):
206
227
  # loop to completion and closes; nothing is awaited/closed here.
207
228
  self._reusable = False
208
229
  self._shutdown_evt.set()
230
+ if self._native_read_interrupt and self._idle_read_parked:
231
+ # asyncio: wake the read parked idly between requests so it returns promptly and
232
+ # the connection closes, instead of relying on a per-read select race (tonio's path).
233
+ self.transport.interrupt_read()
209
234
 
210
235
  def mark_unusable(self):
211
236
  self._reusable = False
@@ -314,22 +339,17 @@ class ServerConnection(H1ConnectionBase):
314
339
  return False
315
340
 
316
341
  async def _read_request_head(self, codec, initial=b""):
317
- # Bound the head read by `header_read_timeout` (slowloris defence, hyper
318
- # http1.rs L249): race the read against a deadline; if it wins, close with no
319
- # response (hyper closes on a header-read timeout). `select` cancels the losing
320
- # branch, cleanly dropping the parked read. `None` disables the timeout.
321
- # NIT (design cleanup, deferred): this `sleep` + `select` deadline could become a
322
- # normalized `backend.timeout(coro, seconds)` primitive — more direct, no
323
- # sentinel / nested select. Kept as-is for now (see TODONOTES).
342
+ # Bound the head read by `header_read_timeout` (slowloris defence, hyper http1.rs L249):
343
+ # if the deadline wins, close with no response (hyper closes on a header-read timeout).
344
+ # `backend.timeout(coro, seconds) -> (result, completed)` cancels the read cleanly on
345
+ # expiry — cheap on every backend (asyncio: one task + one timer; tonio: its native
346
+ # timeout). `None` disables the deadline.
324
347
  if self._header_read_timeout is None:
325
348
  return await self._read_head_frames(codec, initial)
326
-
327
- async def _deadline():
328
- await self.backend.sleep(self._header_read_timeout)
329
- return _TIMED_OUT
330
-
331
- result = await self.backend.select(self._read_head_frames(codec, initial), _deadline())
332
- if result is _TIMED_OUT:
349
+ result, completed = await self.backend.timeout(
350
+ self._read_head_frames(codec, initial), self._header_read_timeout
351
+ )
352
+ if not completed:
333
353
  self._closed = True
334
354
  self._reusable = False
335
355
  self._close_transport()
@@ -390,6 +410,20 @@ class ServerConnection(H1ConnectionBase):
390
410
  if self._shutdown_evt.is_set():
391
411
  return _SHUTDOWN
392
412
 
413
+ 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`.
393
427
  def _recv():
394
428
  return self.transport.receive_some(n)
395
429
 
@@ -36,7 +36,7 @@ requires-python = ">=3.10"
36
36
  dependencies = []
37
37
 
38
38
  [project.optional-dependencies]
39
- tonio = ["tonio~=0.7.1; python_version >= '3.14'"]
39
+ tonio = ["tonio~=0.8.1; python_version >= '3.14'"]
40
40
 
41
41
  [dependency-groups]
42
42
  build = [
@@ -276,6 +276,35 @@ async def test_server_header_read_timeout_closes_slow_head():
276
276
  s.cancel()
277
277
 
278
278
 
279
+ @pytest.mark.tonio
280
+ async def test_server_detach_hands_off_open_connection():
281
+ """detach() stops the accept loop and relinquishes the transport WITHOUT closing it,
282
+ returning bytes read past the request head — so a caller can take over the raw connection
283
+ for a protocol upgrade (e.g. WebSocket)."""
284
+ listener, host, port = await _listener()
285
+ captured = {}
286
+
287
+ async def serve():
288
+ transport = await listener.accept()
289
+ async with H1Server(transport) as server:
290
+ async for req in server:
291
+ captured["leftover"] = req.detach() # loop ends next (detached), socket stays open
292
+ # `__aexit__` ran (close() is a no-op after detach) — the transport is still usable, so
293
+ # the caller drives its own upgrade response on the raw connection.
294
+ await transport.send_all(b"HTTP/1.1 101 Switching Protocols\r\nupgrade: custom\r\n\r\nOWNED")
295
+
296
+ async with scope() as s:
297
+ s.spawn(serve())
298
+ client = await _raw_client(host, port)
299
+ # request head + trailing bytes the client sent past it (must survive as `leftover`)
300
+ await client.send_all(b"GET /up HTTP/1.1\r\nhost: x\r\nupgrade: custom\r\nconnection: upgrade\r\n\r\nEXTRA")
301
+ data = await _read_until(client, b"OWNED")
302
+ s.cancel()
303
+
304
+ assert captured["leftover"] == b"EXTRA" # bytes past the head handed back
305
+ assert b"101 Switching Protocols" in data and data.endswith(b"OWNED") # socket stayed open
306
+
307
+
279
308
  @pytest.mark.tonio
280
309
  async def test_server_no_100_continue_for_http10():
281
310
  """An HTTP/1.0 request never gets an auto 100-continue, even with `Expect:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes