httpunk 0.2.2__tar.gz → 0.4.0__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 (170) hide show
  1. {httpunk-0.2.2 → httpunk-0.4.0}/Cargo.lock +3 -3
  2. {httpunk-0.2.2 → httpunk-0.4.0}/Cargo.toml +2 -2
  3. {httpunk-0.2.2 → httpunk-0.4.0}/PKG-INFO +95 -22
  4. {httpunk-0.2.2 → httpunk-0.4.0}/README.md +93 -20
  5. httpunk-0.4.0/crates/vendor-h2/src/error.rs +211 -0
  6. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/src/lib.rs +12 -4
  7. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/src/proto/mod.rs +13 -2
  8. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/src/proto/streams/mod.rs +2 -1
  9. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/src/proto/streams/state.rs +22 -1
  10. httpunk-0.4.0/crates/vendor-hyper/UPSTREAM_VERSION +1 -0
  11. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-hyper/src/body/length.rs +3 -3
  12. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-hyper/src/common/date.rs +0 -8
  13. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-hyper/src/error.rs +1 -1
  14. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-hyper/src/headers.rs +63 -0
  15. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-hyper/src/lib.rs +6 -3
  16. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-hyper/src/proto/h1/decode.rs +11 -11
  17. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-hyper/src/proto/h1/encode.rs +26 -26
  18. httpunk-0.4.0/crates/vendor-hyper/src/proto/h1/httpunk.rs +763 -0
  19. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-hyper/src/proto/h1/role.rs +47 -10
  20. {httpunk-0.2.2 → httpunk-0.4.0}/httpunk/__init__.py +7 -1
  21. {httpunk-0.2.2 → httpunk-0.4.0}/httpunk/_backend/asyncio.py +163 -33
  22. httpunk-0.4.0/httpunk/_backend/tonio.py +298 -0
  23. {httpunk-0.2.2 → httpunk-0.4.0}/httpunk/_common.py +28 -6
  24. httpunk-0.4.0/httpunk/_httpunk.pyi +1133 -0
  25. {httpunk-0.2.2 → httpunk-0.4.0}/httpunk/asyncio.py +34 -20
  26. {httpunk-0.2.2 → httpunk-0.4.0}/httpunk/exceptions.py +42 -15
  27. {httpunk-0.2.2 → httpunk-0.4.0}/httpunk/h1/__init__.py +1 -1
  28. httpunk-0.4.0/httpunk/h1/client.py +607 -0
  29. httpunk-0.4.0/httpunk/h1/connection.py +94 -0
  30. httpunk-0.4.0/httpunk/h1/server.py +1060 -0
  31. {httpunk-0.2.2 → httpunk-0.4.0}/httpunk/h1/share.py +67 -29
  32. httpunk-0.4.0/httpunk/h2/__init__.py +2 -0
  33. httpunk-0.4.0/httpunk/h2/client.py +283 -0
  34. httpunk-0.4.0/httpunk/h2/connection.py +554 -0
  35. httpunk-0.4.0/httpunk/h2/server.py +405 -0
  36. httpunk-0.4.0/httpunk/h2/share.py +43 -0
  37. httpunk-0.4.0/httpunk/h2/stream.py +45 -0
  38. {httpunk-0.2.2 → httpunk-0.4.0}/httpunk/types.py +17 -1
  39. {httpunk-0.2.2 → httpunk-0.4.0}/httpunk/util/__init__.py +1 -1
  40. httpunk-0.4.0/httpunk/util/auto.py +311 -0
  41. {httpunk-0.2.2 → httpunk-0.4.0}/httpunk/util/client.py +12 -10
  42. {httpunk-0.2.2 → httpunk-0.4.0}/httpunk/util/graceful.py +14 -7
  43. {httpunk-0.2.2 → httpunk-0.4.0}/httpunk/util/pool.py +85 -41
  44. {httpunk-0.2.2 → httpunk-0.4.0}/pyproject.toml +1 -1
  45. {httpunk-0.2.2 → httpunk-0.4.0}/src/errors.rs +9 -5
  46. httpunk-0.4.0/src/h1/codec.rs +739 -0
  47. httpunk-0.4.0/src/h1/conn.rs +1470 -0
  48. httpunk-0.4.0/src/h1/errors.rs +154 -0
  49. {httpunk-0.2.2 → httpunk-0.4.0}/src/h1/mod.rs +12 -0
  50. {httpunk-0.2.2 → httpunk-0.4.0}/src/h2/codec.rs +618 -210
  51. httpunk-0.4.0/src/h2/conn.rs +2385 -0
  52. httpunk-0.4.0/src/h2/errors.rs +122 -0
  53. httpunk-0.4.0/src/h2/mod.rs +90 -0
  54. httpunk-0.4.0/src/h2/reason.rs +192 -0
  55. httpunk-0.4.0/src/h2/settings.rs +104 -0
  56. httpunk-0.4.0/src/h2/streams.rs +530 -0
  57. {httpunk-0.2.2 → httpunk-0.4.0}/src/http/mod.rs +62 -1
  58. {httpunk-0.2.2 → httpunk-0.4.0}/src/lib.rs +1 -0
  59. httpunk-0.4.0/src/py.rs +105 -0
  60. httpunk-0.4.0/tests/_transport.py +24 -0
  61. {httpunk-0.2.2 → httpunk-0.4.0}/tests/test_asyncio_backend.py +121 -5
  62. httpunk-0.4.0/tests/test_asyncio_e2e.py +596 -0
  63. httpunk-0.4.0/tests/test_backend.py +248 -0
  64. {httpunk-0.2.2 → httpunk-0.4.0}/tests/test_error_retention.py +3 -0
  65. httpunk-0.4.0/tests/test_h1_codec.py +380 -0
  66. {httpunk-0.2.2 → httpunk-0.4.0}/tests/test_h1_conn.py +190 -8
  67. {httpunk-0.2.2 → httpunk-0.4.0}/tests/test_h1_decode.py +57 -2
  68. httpunk-0.4.0/tests/test_h1_server.py +2022 -0
  69. {httpunk-0.2.2 → httpunk-0.4.0}/tests/test_h2_codec.py +33 -0
  70. {httpunk-0.2.2 → httpunk-0.4.0}/tests/test_h2_conn.py +2 -0
  71. {httpunk-0.2.2 → httpunk-0.4.0}/tests/test_h2_errors.py +6 -4
  72. {httpunk-0.2.2 → httpunk-0.4.0}/tests/test_h2_flow.py +107 -91
  73. httpunk-0.4.0/tests/test_h2_flow_control.py +174 -0
  74. httpunk-0.4.0/tests/test_h2_ready.py +91 -0
  75. {httpunk-0.2.2 → httpunk-0.4.0}/tests/test_h2_server.py +452 -22
  76. httpunk-0.4.0/tests/test_h2_settings.py +115 -0
  77. httpunk-0.4.0/tests/test_h2_write_batch.py +115 -0
  78. {httpunk-0.2.2 → httpunk-0.4.0}/tests/test_headermap.py +10 -0
  79. httpunk-0.4.0/tests/test_util_auto.py +307 -0
  80. {httpunk-0.2.2 → httpunk-0.4.0}/tests/test_util_connect.py +14 -1
  81. {httpunk-0.2.2 → httpunk-0.4.0}/tests/test_util_graceful.py +157 -39
  82. {httpunk-0.2.2 → httpunk-0.4.0}/tests/test_util_pool.py +97 -2
  83. httpunk-0.4.0/tests/test_util_tls.py +397 -0
  84. httpunk-0.2.2/crates/vendor-hyper/UPSTREAM_VERSION +0 -1
  85. httpunk-0.2.2/crates/vendor-hyper/src/proto/h1/httpunk.rs +0 -449
  86. httpunk-0.2.2/httpunk/_backend/tonio.py +0 -142
  87. httpunk-0.2.2/httpunk/_httpunk.pyi +0 -461
  88. httpunk-0.2.2/httpunk/h1/client.py +0 -654
  89. httpunk-0.2.2/httpunk/h1/connection.py +0 -140
  90. httpunk-0.2.2/httpunk/h1/server.py +0 -576
  91. httpunk-0.2.2/httpunk/h2/__init__.py +0 -2
  92. httpunk-0.2.2/httpunk/h2/client.py +0 -463
  93. httpunk-0.2.2/httpunk/h2/connection.py +0 -522
  94. httpunk-0.2.2/httpunk/h2/server.py +0 -474
  95. httpunk-0.2.2/httpunk/h2/settings.py +0 -167
  96. httpunk-0.2.2/httpunk/h2/share.py +0 -88
  97. httpunk-0.2.2/httpunk/h2/stream.py +0 -91
  98. httpunk-0.2.2/httpunk/h2/streams.py +0 -1058
  99. httpunk-0.2.2/httpunk/util/auto.py +0 -123
  100. httpunk-0.2.2/src/h1/codec.rs +0 -414
  101. httpunk-0.2.2/src/h2/mod.rs +0 -74
  102. httpunk-0.2.2/src/h2/streams.rs +0 -357
  103. httpunk-0.2.2/tests/test_asyncio_e2e.py +0 -292
  104. httpunk-0.2.2/tests/test_backend.py +0 -119
  105. httpunk-0.2.2/tests/test_h1_codec.py +0 -108
  106. httpunk-0.2.2/tests/test_h1_server.py +0 -727
  107. httpunk-0.2.2/tests/test_h2_flow_control.py +0 -74
  108. httpunk-0.2.2/tests/test_h2_ready.py +0 -85
  109. httpunk-0.2.2/tests/test_h2_settings.py +0 -115
  110. httpunk-0.2.2/tests/test_util_auto.py +0 -161
  111. httpunk-0.2.2/tests/test_util_tls.py +0 -111
  112. {httpunk-0.2.2 → httpunk-0.4.0}/LICENSE +0 -0
  113. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/Cargo.toml +0 -0
  114. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/LICENSE +0 -0
  115. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/UPSTREAM_VERSION +0 -0
  116. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/src/codec/error.rs +0 -0
  117. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/src/codec/mod.rs +0 -0
  118. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/src/ext.rs +0 -0
  119. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/src/frame/data.rs +0 -0
  120. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/src/frame/go_away.rs +0 -0
  121. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/src/frame/head.rs +0 -0
  122. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/src/frame/headers.rs +0 -0
  123. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/src/frame/mod.rs +0 -0
  124. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/src/frame/ping.rs +0 -0
  125. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/src/frame/priority.rs +0 -0
  126. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/src/frame/reason.rs +0 -0
  127. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/src/frame/reset.rs +0 -0
  128. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/src/frame/settings.rs +0 -0
  129. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/src/frame/stream_id.rs +0 -0
  130. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/src/frame/util.rs +0 -0
  131. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/src/frame/window_update.rs +0 -0
  132. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/src/hpack/decoder.rs +0 -0
  133. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/src/hpack/encoder.rs +0 -0
  134. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/src/hpack/header.rs +0 -0
  135. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/src/hpack/huffman/mod.rs +0 -0
  136. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/src/hpack/huffman/table.rs +0 -0
  137. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/src/hpack/mod.rs +0 -0
  138. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/src/hpack/table.rs +0 -0
  139. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/src/proto/error.rs +0 -0
  140. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-h2/src/proto/streams/flow_control.rs +0 -0
  141. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-hyper/Cargo.toml +0 -0
  142. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-hyper/LICENSE +0 -0
  143. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-hyper/src/body/mod.rs +0 -0
  144. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-hyper/src/cfg.rs +0 -0
  145. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-hyper/src/common/mod.rs +0 -0
  146. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-hyper/src/ext/h1_reason_phrase.rs +0 -0
  147. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-hyper/src/ext/informational.rs +0 -0
  148. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-hyper/src/ext/mod.rs +0 -0
  149. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-hyper/src/proto/h1/io.rs +0 -0
  150. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-hyper/src/proto/h1/mod.rs +0 -0
  151. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-hyper/src/proto/mod.rs +0 -0
  152. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-hyper/src/trace.rs +0 -0
  153. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-hyper-util/Cargo.toml +0 -0
  154. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-hyper-util/LICENSE +0 -0
  155. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-hyper-util/UPSTREAM_VERSION +0 -0
  156. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-hyper-util/src/client/mod.rs +0 -0
  157. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-hyper-util/src/client/proxy/matcher.rs +0 -0
  158. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-hyper-util/src/client/proxy/mod.rs +0 -0
  159. {httpunk-0.2.2 → httpunk-0.4.0}/crates/vendor-hyper-util/src/lib.rs +0 -0
  160. {httpunk-0.2.2 → httpunk-0.4.0}/httpunk/_backend/__init__.py +0 -0
  161. {httpunk-0.2.2 → httpunk-0.4.0}/httpunk/http.py +0 -0
  162. {httpunk-0.2.2 → httpunk-0.4.0}/httpunk/py.typed +0 -0
  163. {httpunk-0.2.2 → httpunk-0.4.0}/httpunk/util/proxy.py +0 -0
  164. {httpunk-0.2.2 → httpunk-0.4.0}/src/proxy.rs +0 -0
  165. {httpunk-0.2.2 → httpunk-0.4.0}/tests/_client.py +0 -0
  166. {httpunk-0.2.2 → httpunk-0.4.0}/tests/conftest.py +0 -0
  167. {httpunk-0.2.2 → httpunk-0.4.0}/tests/test_asyncio_client_protocol.py +0 -0
  168. {httpunk-0.2.2 → httpunk-0.4.0}/tests/test_asyncio_protocol.py +0 -0
  169. {httpunk-0.2.2 → httpunk-0.4.0}/tests/test_h2_state.py +0 -0
  170. {httpunk-0.2.2 → httpunk-0.4.0}/tests/test_util_proxy.py +0 -0
@@ -118,7 +118,7 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
118
118
 
119
119
  [[package]]
120
120
  name = "httpunk"
121
- version = "0.2.2"
121
+ version = "0.4.0"
122
122
  dependencies = [
123
123
  "bytes",
124
124
  "h2",
@@ -132,9 +132,9 @@ dependencies = [
132
132
 
133
133
  [[package]]
134
134
  name = "hyper"
135
- version = "1.11.0"
135
+ version = "1.11.1"
136
136
  source = "registry+https://github.com/rust-lang/crates.io-index"
137
- checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72"
137
+ checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43"
138
138
  dependencies = [
139
139
  "atomic-waker",
140
140
  "bytes",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "httpunk"
3
- version = "0.2.2"
3
+ version = "0.4.0"
4
4
  description = "The Rust HTTP library for Python"
5
5
  authors = [
6
6
  "Giovanni Barillari <g@baro.dev>",
@@ -61,7 +61,7 @@ vendor-hyper-util = { path = "crates/vendor-hyper-util" }
61
61
  # the pinned versions land in Cargo.lock for `cargo audit` / Dependabot advisory
62
62
  # coverage without pulling them (and tokio) into the build.
63
63
  h2 = { version = "=0.4.19", optional = true }
64
- hyper = { version = "=1.11.0", optional = true, default-features = false, features = ["http1", "client", "server"] }
64
+ hyper = { version = "=1.11.1", optional = true, default-features = false, features = ["http1", "client", "server"] }
65
65
 
66
66
  [profile.release]
67
67
  codegen-units = 1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: httpunk
3
- Version: 0.2.2
3
+ Version: 0.4.0
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.12 ; python_full_version >= '3.14' and extra == 'tonio'
23
+ Requires-Dist: tonio~=0.9.15 ; 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
@@ -183,6 +183,7 @@ async for chunk in resp.aiter_bytes():
183
183
  ...
184
184
 
185
185
  resp.trailers # a HeaderMap of trailing headers, or None
186
+ resp.version # httpunk.Version.HTTP_2 / HTTP_11 / HTTP_10 (≈ http::Version)
186
187
  ```
187
188
 
188
189
  A response can be used as an async context manager to guarantee release (cancelling the body
@@ -198,7 +199,9 @@ async with await conn.request("GET", "/big") as resp:
198
199
 
199
200
  `body` may be `bytes`, or a sync/async iterable of `bytes` (streamed as it is produced).
200
201
  `trailers` are header fields sent after the body — chunked trailers on HTTP/1, a trailing
201
- `HEADERS` frame on HTTP/2:
202
+ `HEADERS` frame on HTTP/2. As in hyper, HTTP/1 trailers ride only on a chunked body (a
203
+ streamed one; a `bytes` body is `Content-Length`-framed and drops them) and only the fields
204
+ the request's own `Trailer` header declares are sent — undeclared ones are dropped:
202
205
 
203
206
  ```python
204
207
  async def chunks():
@@ -207,7 +210,7 @@ async def chunks():
207
210
 
208
211
  resp = await conn.request(
209
212
  "POST", "/upload",
210
- headers={"host": "example.com", "content-type": "application/octet-stream"},
213
+ headers={"host": "example.com", "content-type": "application/octet-stream", "trailer": "x-checksum"},
211
214
  body=chunks(),
212
215
  trailers={"x-checksum": "..."},
213
216
  )
@@ -233,11 +236,51 @@ async with H1Server(transport, backend=Backend.asyncio) as server:
233
236
  await request.respond(200, headers={"content-type": "text/plain"}, body=body)
234
237
  ```
235
238
 
236
- Each `request` carries `method`, `target`/`path`, `headers`, and a streamable body
239
+ Each `request` carries `method`, `target`/`path`, `headers`, `version`, and a streamable body
237
240
  (`request.read()` / `request.aiter_bytes()`). Answer it with `request.respond(status, *,
238
- headers=None, body=None)`. On HTTP/2 you can also abort a single stream with
239
- `request.reset()` instead of responding (e.g. when a handler fails) — the connection and its
240
- other streams keep running.
241
+ headers=None, body=None, trailers=None)` — `trailers` are sent after the body (HTTP/1.1
242
+ chunked trailers; an HTTP/2 trailing HEADERS frame), like the client's `Request.trailers` and
243
+ under hyper's rules: on HTTP/1.1 they need a chunked (streamed) body and a `Trailer` header
244
+ declaring the fields, and go out only if the request declared `TE: trailers`; otherwise they
245
+ are dropped and the body ends normally, as hyper's server does.
246
+ On HTTP/2 you can also abort a single stream with `request.reset()` instead of responding
247
+ (e.g. when a handler fails) — the connection and its other streams keep running — and
248
+ `await request.reset_received()` resolves with the reason
249
+ when the *client* abandons the request (`RST_STREAM`, even after it finished sending), the
250
+ signal a long-running or streaming handler races against its own completion to stop early.
251
+ A reset also fails an in-flight `respond()` / `send_data` with `StreamResetError` carrying the
252
+ client's reason, including while the body is waiting on the app's next chunk. The HTTP/1 twin
253
+ is `await request.peer_closed()`: once the request body is complete, a client that closes the
254
+ connection before the response is done is detected (hyper's `mid_message_detect_eof`), the
255
+ in-flight `respond()` / `send_data` fail with `H1IncompleteMessageError`, and the accept loop
256
+ ends. The await resolves either way and never hangs: `True` when the client closed
257
+ mid-request, `False` once the exchange completed (or failed) first, at once if it already had.
258
+ `H1Server(half_close=True)` turns the detection off, as hyper's `half_close` does. A request
259
+ announcing an upgrade (`Upgrade: h2c`, `Upgrade: websocket`, CONNECT) is watched only from its
260
+ response head on, when it can no longer be detached or switched.
261
+
262
+ When `respond()` fails this way while streaming an async body, the failure is raised at once
263
+ even if the body is parked waiting for its next chunk: the producer is cancelled at that
264
+ `await` (as hyper drops the body future), its `finally` blocks run, and only then does
265
+ `respond()` raise. Cancelling the task that awaits `respond()` cancels the producer the same way.
266
+
267
+ For push-style producers (an ASGI `send()` loop, a server-sent-events endpoint) use
268
+ `request.send_response(status, *, headers=None, end_stream=False)`: it writes the head now and
269
+ returns a `SendStream` — h2's `SendResponse`/`SendStream` shape, identical on both protocols:
270
+
271
+ ```python
272
+ stream = await request.send_response(200, headers={"content-type": "text/event-stream"})
273
+ await stream.send_data(b"data: 1\n\n") # each write awaits backpressure
274
+ await stream.send_data(b"data: 2\n\n", end_stream=True)
275
+ # stream.send_trailers({...}) ends the body with trailers; stream.send_reset() aborts it
276
+ ```
277
+
278
+ `end_stream=True` on `send_response` is a bodyless response. Framing follows hyper: a
279
+ `content-length` you set is honoured, otherwise the body is chunked (HTTP/1.1) or
280
+ close-delimited (HTTP/1.0); on HEAD/204/304 body chunks are discarded, as hyper never polls
281
+ that body. `send_reset` is `RST_STREAM(CANCEL)` on HTTP/2; HTTP/1 has no per-request reset, so
282
+ there it closes the connection (what hyper does when a response body errors). `respond()` is
283
+ the pull convenience built on the same path.
241
284
 
242
285
  HTTP/1 serves one request/response at a time (the loop won't yield the next until the current
243
286
  one is answered); HTTP/2 multiplexes, so for concurrent handling you would spawn a task per
@@ -275,24 +318,32 @@ h.raw_items() # [(b'content-type', b'text/plain'), (b'set-cookie'
275
318
 
276
319
  ### Errors
277
320
 
278
- httpunk's exceptions all derive from a common `HTTPunkError` root. `ConnectionClosedError`
279
- is **protocol-neutral** — raised on both HTTP/1 and HTTP/2 when the transport closes with
280
- work in flight — so it sits directly under the root. Every **HTTP/2-specific** error shares
281
- the `H2Error` sub-base:
321
+ httpunk's exceptions all derive from a common `HTTPunkError` root, with one class per
322
+ public error kind of the upstream crate, so a caller can match on what hyper or h2 would
323
+ have reported. `ConnectionClosedError` is **protocol-neutral** — hyper's `Io` and h2's
324
+ `Io`: the transport failed with work in flight — so it sits directly under the root. The
325
+ **HTTP/1** errors mirror hyper's `Error` kinds under `H1Error`; the **HTTP/2** errors
326
+ mirror the h2 crate's under `H2Error`:
282
327
 
283
328
  ```
284
329
  HTTPunkError
285
- ├── ConnectionClosedError transport closed / IO error with work in flight (HTTP/1 + HTTP/2)
286
- └── H2Error base for HTTP/2 protocol errors
287
- ├── H2ProtocolError connection-level protocol violation (-> GOAWAY)
288
- ├── H2StreamError stream-level protocol violation (-> RST_STREAM)
289
- ├── H2UserError local API misuse
290
- ├── H2FlowControlError flow-control window over/underflow
291
- ├── GoAwayError the peer sent GOAWAY
292
- └── StreamResetError the peer sent RST_STREAM for a stream
330
+ ├── ConnectionClosedError transport closed / IO error with work in flight (hyper Io, h2 Io)
331
+ ├── H1Error base for HTTP/1 errors (hyper `Error` kinds)
332
+ │ ├── H1ParseError malformed message head (hyper Parse) — args = (kind, message)
333
+ │ ├── H1BodyError malformed or truncated body (hyper Body) — args = (io_kind, message)
334
+ │ ├── H1IncompleteMessageError EOF while a message was still expected (hyper IncompleteMessage)
335
+ │ ├── H1UnexpectedMessageError bytes on an idle client connection (hyper UnexpectedMessage)
336
+ │ └── H1UserError local misuse hyper reports on the wire path (hyper User) — args = (kind, message)
337
+ └── H2Error base for HTTP/2 protocol errors
338
+ ├── H2ProtocolError connection-level protocol violation (-> GOAWAY)
339
+ ├── H2StreamError stream-level protocol violation (-> RST_STREAM)
340
+ ├── H2UserError local API misuse
341
+ ├── H2FlowControlError flow-control window over/underflow
342
+ ├── GoAwayError the peer sent GOAWAY
343
+ └── StreamResetError the peer sent RST_STREAM for a stream
293
344
  ```
294
345
 
295
- Catch `H2Error` for HTTP/2 protocol failures, `ConnectionClosedError` for a dropped
346
+ Catch `H1Error` / `H2Error` for protocol failures, `ConnectionClosedError` for a dropped
296
347
  transport, or `HTTPunkError` for anything httpunk raises.
297
348
 
298
349
  `GoAwayError` carries `last_stream_id`, `error_code` and `debug_data`; `StreamResetError`
@@ -300,7 +351,7 @@ carries `stream_id` and `error_code`. Error codes are `H2Reason` members (an `In
300
351
  they compare equal to plain ints) for known codes, or a raw int otherwise.
301
352
 
302
353
  ```python
303
- from httpunk import ConnectionClosedError, GoAwayError, HTTPunkError, StreamResetError
354
+ from httpunk import ConnectionClosedError, GoAwayError, H1BodyError, HTTPunkError, StreamResetError
304
355
 
305
356
  try:
306
357
  resp = await conn.request("GET", "/")
@@ -310,6 +361,9 @@ except StreamResetError as exc:
310
361
  except GoAwayError as exc:
311
362
  # streams above last_stream_id were not processed and are safe to retry
312
363
  print("server going away:", exc.last_stream_id)
364
+ except H1BodyError as exc:
365
+ io_kind, message = exc.args
366
+ print("truncated" if io_kind == "unexpected_eof" else "malformed body")
313
367
  except ConnectionClosedError:
314
368
  print("transport dropped")
315
369
  except HTTPunkError:
@@ -355,6 +409,25 @@ async with server:
355
409
  await request.respond(200, body=b"ok")
356
410
  ```
357
411
 
412
+ To configure per-protocol options, use `auto.Builder` (hyper-util's `auto::Builder`): `http1()` /
413
+ `http2()` return sub-builders whose setters chain, `http1_only()` / `http2_only()` force a protocol,
414
+ and `serve_connection(transport, cancel=None)` sniffs and builds. `serve()` is the default-options
415
+ shortcut over it.
416
+
417
+ ```python
418
+ builder = auto.Builder(backend=Backend.asyncio)
419
+ builder.http1().header_read_timeout(5.0).http2().max_concurrent_streams(200)
420
+ server = await builder.serve_connection(transport)
421
+ ```
422
+
423
+ The option set mirrors hyper's server builders, with hyper's defaults. HTTP/1
424
+ (`http1::Builder`): `header_read_timeout`, `keep_alive`, `max_headers`, `max_buf_size`,
425
+ `auto_date_header`, `title_case_headers`, `ignore_invalid_headers`. HTTP/2 (`http2::Builder`):
426
+ `max_concurrent_streams`, `initial_stream_window_size`, `initial_connection_window_size`,
427
+ `max_frame_size`, `max_header_list_size`, `max_pending_accept_reset_streams`,
428
+ `max_local_error_reset_streams`, `auto_date_header`, `max_send_buf_size`, plus h2's
429
+ `data_frame_budget`. The same keywords are accepted by `H1Server(...)` / `H2Server(...)` directly.
430
+
358
431
  #### Connection pools
359
432
 
360
433
  `httpunk.util.pool` provides three composable pools. A *connector* is an async callable
@@ -147,6 +147,7 @@ async for chunk in resp.aiter_bytes():
147
147
  ...
148
148
 
149
149
  resp.trailers # a HeaderMap of trailing headers, or None
150
+ resp.version # httpunk.Version.HTTP_2 / HTTP_11 / HTTP_10 (≈ http::Version)
150
151
  ```
151
152
 
152
153
  A response can be used as an async context manager to guarantee release (cancelling the body
@@ -162,7 +163,9 @@ async with await conn.request("GET", "/big") as resp:
162
163
 
163
164
  `body` may be `bytes`, or a sync/async iterable of `bytes` (streamed as it is produced).
164
165
  `trailers` are header fields sent after the body — chunked trailers on HTTP/1, a trailing
165
- `HEADERS` frame on HTTP/2:
166
+ `HEADERS` frame on HTTP/2. As in hyper, HTTP/1 trailers ride only on a chunked body (a
167
+ streamed one; a `bytes` body is `Content-Length`-framed and drops them) and only the fields
168
+ the request's own `Trailer` header declares are sent — undeclared ones are dropped:
166
169
 
167
170
  ```python
168
171
  async def chunks():
@@ -171,7 +174,7 @@ async def chunks():
171
174
 
172
175
  resp = await conn.request(
173
176
  "POST", "/upload",
174
- headers={"host": "example.com", "content-type": "application/octet-stream"},
177
+ headers={"host": "example.com", "content-type": "application/octet-stream", "trailer": "x-checksum"},
175
178
  body=chunks(),
176
179
  trailers={"x-checksum": "..."},
177
180
  )
@@ -197,11 +200,51 @@ async with H1Server(transport, backend=Backend.asyncio) as server:
197
200
  await request.respond(200, headers={"content-type": "text/plain"}, body=body)
198
201
  ```
199
202
 
200
- Each `request` carries `method`, `target`/`path`, `headers`, and a streamable body
203
+ Each `request` carries `method`, `target`/`path`, `headers`, `version`, and a streamable body
201
204
  (`request.read()` / `request.aiter_bytes()`). Answer it with `request.respond(status, *,
202
- headers=None, body=None)`. On HTTP/2 you can also abort a single stream with
203
- `request.reset()` instead of responding (e.g. when a handler fails) — the connection and its
204
- other streams keep running.
205
+ headers=None, body=None, trailers=None)` — `trailers` are sent after the body (HTTP/1.1
206
+ chunked trailers; an HTTP/2 trailing HEADERS frame), like the client's `Request.trailers` and
207
+ under hyper's rules: on HTTP/1.1 they need a chunked (streamed) body and a `Trailer` header
208
+ declaring the fields, and go out only if the request declared `TE: trailers`; otherwise they
209
+ are dropped and the body ends normally, as hyper's server does.
210
+ On HTTP/2 you can also abort a single stream with `request.reset()` instead of responding
211
+ (e.g. when a handler fails) — the connection and its other streams keep running — and
212
+ `await request.reset_received()` resolves with the reason
213
+ when the *client* abandons the request (`RST_STREAM`, even after it finished sending), the
214
+ signal a long-running or streaming handler races against its own completion to stop early.
215
+ A reset also fails an in-flight `respond()` / `send_data` with `StreamResetError` carrying the
216
+ client's reason, including while the body is waiting on the app's next chunk. The HTTP/1 twin
217
+ is `await request.peer_closed()`: once the request body is complete, a client that closes the
218
+ connection before the response is done is detected (hyper's `mid_message_detect_eof`), the
219
+ in-flight `respond()` / `send_data` fail with `H1IncompleteMessageError`, and the accept loop
220
+ ends. The await resolves either way and never hangs: `True` when the client closed
221
+ mid-request, `False` once the exchange completed (or failed) first, at once if it already had.
222
+ `H1Server(half_close=True)` turns the detection off, as hyper's `half_close` does. A request
223
+ announcing an upgrade (`Upgrade: h2c`, `Upgrade: websocket`, CONNECT) is watched only from its
224
+ response head on, when it can no longer be detached or switched.
225
+
226
+ When `respond()` fails this way while streaming an async body, the failure is raised at once
227
+ even if the body is parked waiting for its next chunk: the producer is cancelled at that
228
+ `await` (as hyper drops the body future), its `finally` blocks run, and only then does
229
+ `respond()` raise. Cancelling the task that awaits `respond()` cancels the producer the same way.
230
+
231
+ For push-style producers (an ASGI `send()` loop, a server-sent-events endpoint) use
232
+ `request.send_response(status, *, headers=None, end_stream=False)`: it writes the head now and
233
+ returns a `SendStream` — h2's `SendResponse`/`SendStream` shape, identical on both protocols:
234
+
235
+ ```python
236
+ stream = await request.send_response(200, headers={"content-type": "text/event-stream"})
237
+ await stream.send_data(b"data: 1\n\n") # each write awaits backpressure
238
+ await stream.send_data(b"data: 2\n\n", end_stream=True)
239
+ # stream.send_trailers({...}) ends the body with trailers; stream.send_reset() aborts it
240
+ ```
241
+
242
+ `end_stream=True` on `send_response` is a bodyless response. Framing follows hyper: a
243
+ `content-length` you set is honoured, otherwise the body is chunked (HTTP/1.1) or
244
+ close-delimited (HTTP/1.0); on HEAD/204/304 body chunks are discarded, as hyper never polls
245
+ that body. `send_reset` is `RST_STREAM(CANCEL)` on HTTP/2; HTTP/1 has no per-request reset, so
246
+ there it closes the connection (what hyper does when a response body errors). `respond()` is
247
+ the pull convenience built on the same path.
205
248
 
206
249
  HTTP/1 serves one request/response at a time (the loop won't yield the next until the current
207
250
  one is answered); HTTP/2 multiplexes, so for concurrent handling you would spawn a task per
@@ -239,24 +282,32 @@ h.raw_items() # [(b'content-type', b'text/plain'), (b'set-cookie'
239
282
 
240
283
  ### Errors
241
284
 
242
- httpunk's exceptions all derive from a common `HTTPunkError` root. `ConnectionClosedError`
243
- is **protocol-neutral** — raised on both HTTP/1 and HTTP/2 when the transport closes with
244
- work in flight — so it sits directly under the root. Every **HTTP/2-specific** error shares
245
- the `H2Error` sub-base:
285
+ httpunk's exceptions all derive from a common `HTTPunkError` root, with one class per
286
+ public error kind of the upstream crate, so a caller can match on what hyper or h2 would
287
+ have reported. `ConnectionClosedError` is **protocol-neutral** — hyper's `Io` and h2's
288
+ `Io`: the transport failed with work in flight — so it sits directly under the root. The
289
+ **HTTP/1** errors mirror hyper's `Error` kinds under `H1Error`; the **HTTP/2** errors
290
+ mirror the h2 crate's under `H2Error`:
246
291
 
247
292
  ```
248
293
  HTTPunkError
249
- ├── ConnectionClosedError transport closed / IO error with work in flight (HTTP/1 + HTTP/2)
250
- └── H2Error base for HTTP/2 protocol errors
251
- ├── H2ProtocolError connection-level protocol violation (-> GOAWAY)
252
- ├── H2StreamError stream-level protocol violation (-> RST_STREAM)
253
- ├── H2UserError local API misuse
254
- ├── H2FlowControlError flow-control window over/underflow
255
- ├── GoAwayError the peer sent GOAWAY
256
- └── StreamResetError the peer sent RST_STREAM for a stream
294
+ ├── ConnectionClosedError transport closed / IO error with work in flight (hyper Io, h2 Io)
295
+ ├── H1Error base for HTTP/1 errors (hyper `Error` kinds)
296
+ │ ├── H1ParseError malformed message head (hyper Parse) — args = (kind, message)
297
+ │ ├── H1BodyError malformed or truncated body (hyper Body) — args = (io_kind, message)
298
+ │ ├── H1IncompleteMessageError EOF while a message was still expected (hyper IncompleteMessage)
299
+ │ ├── H1UnexpectedMessageError bytes on an idle client connection (hyper UnexpectedMessage)
300
+ │ └── H1UserError local misuse hyper reports on the wire path (hyper User) — args = (kind, message)
301
+ └── H2Error base for HTTP/2 protocol errors
302
+ ├── H2ProtocolError connection-level protocol violation (-> GOAWAY)
303
+ ├── H2StreamError stream-level protocol violation (-> RST_STREAM)
304
+ ├── H2UserError local API misuse
305
+ ├── H2FlowControlError flow-control window over/underflow
306
+ ├── GoAwayError the peer sent GOAWAY
307
+ └── StreamResetError the peer sent RST_STREAM for a stream
257
308
  ```
258
309
 
259
- Catch `H2Error` for HTTP/2 protocol failures, `ConnectionClosedError` for a dropped
310
+ Catch `H1Error` / `H2Error` for protocol failures, `ConnectionClosedError` for a dropped
260
311
  transport, or `HTTPunkError` for anything httpunk raises.
261
312
 
262
313
  `GoAwayError` carries `last_stream_id`, `error_code` and `debug_data`; `StreamResetError`
@@ -264,7 +315,7 @@ carries `stream_id` and `error_code`. Error codes are `H2Reason` members (an `In
264
315
  they compare equal to plain ints) for known codes, or a raw int otherwise.
265
316
 
266
317
  ```python
267
- from httpunk import ConnectionClosedError, GoAwayError, HTTPunkError, StreamResetError
318
+ from httpunk import ConnectionClosedError, GoAwayError, H1BodyError, HTTPunkError, StreamResetError
268
319
 
269
320
  try:
270
321
  resp = await conn.request("GET", "/")
@@ -274,6 +325,9 @@ except StreamResetError as exc:
274
325
  except GoAwayError as exc:
275
326
  # streams above last_stream_id were not processed and are safe to retry
276
327
  print("server going away:", exc.last_stream_id)
328
+ except H1BodyError as exc:
329
+ io_kind, message = exc.args
330
+ print("truncated" if io_kind == "unexpected_eof" else "malformed body")
277
331
  except ConnectionClosedError:
278
332
  print("transport dropped")
279
333
  except HTTPunkError:
@@ -319,6 +373,25 @@ async with server:
319
373
  await request.respond(200, body=b"ok")
320
374
  ```
321
375
 
376
+ To configure per-protocol options, use `auto.Builder` (hyper-util's `auto::Builder`): `http1()` /
377
+ `http2()` return sub-builders whose setters chain, `http1_only()` / `http2_only()` force a protocol,
378
+ and `serve_connection(transport, cancel=None)` sniffs and builds. `serve()` is the default-options
379
+ shortcut over it.
380
+
381
+ ```python
382
+ builder = auto.Builder(backend=Backend.asyncio)
383
+ builder.http1().header_read_timeout(5.0).http2().max_concurrent_streams(200)
384
+ server = await builder.serve_connection(transport)
385
+ ```
386
+
387
+ The option set mirrors hyper's server builders, with hyper's defaults. HTTP/1
388
+ (`http1::Builder`): `header_read_timeout`, `keep_alive`, `max_headers`, `max_buf_size`,
389
+ `auto_date_header`, `title_case_headers`, `ignore_invalid_headers`. HTTP/2 (`http2::Builder`):
390
+ `max_concurrent_streams`, `initial_stream_window_size`, `initial_connection_window_size`,
391
+ `max_frame_size`, `max_header_list_size`, `max_pending_accept_reset_streams`,
392
+ `max_local_error_reset_streams`, `auto_date_header`, `max_send_buf_size`, plus h2's
393
+ `data_frame_budget`. The same keywords are accepted by `H1Server(...)` / `H2Server(...)` directly.
394
+
322
395
  #### Connection pools
323
396
 
324
397
  `httpunk.util.pool` provides three composable pools. A *connector* is an async callable
@@ -0,0 +1,211 @@
1
+ use crate::codec::{SendError, UserError};
2
+ use crate::frame::StreamId;
3
+ use crate::proto::{self, Initiator};
4
+
5
+ use bytes::Bytes;
6
+ use std::{error, fmt, io};
7
+
8
+ pub use crate::frame::Reason;
9
+
10
+ /// Represents HTTP/2 operation errors.
11
+ ///
12
+ /// `Error` covers error cases raised by protocol errors caused by the
13
+ /// peer, I/O (transport) errors, and errors caused by the user of the library.
14
+ ///
15
+ /// If the error was caused by the remote peer, then it will contain a
16
+ /// [`Reason`] which can be obtained with the [`reason`] function.
17
+ ///
18
+ /// [`Reason`]: struct.Reason.html
19
+ /// [`reason`]: #method.reason
20
+ #[derive(Debug)]
21
+ pub struct Error {
22
+ kind: Kind,
23
+ }
24
+
25
+ #[derive(Debug)]
26
+ enum Kind {
27
+ /// A RST_STREAM frame was received or sent.
28
+ #[allow(dead_code)]
29
+ Reset(StreamId, Reason, Initiator),
30
+
31
+ /// A GO_AWAY frame was received or sent.
32
+ GoAway(Bytes, Reason, Initiator),
33
+
34
+ /// The user created an error from a bare Reason.
35
+ Reason(Reason),
36
+
37
+ /// An error resulting from an invalid action taken by the user of this
38
+ /// library.
39
+ User(UserError),
40
+
41
+ /// An `io::Error` occurred while trying to read or write.
42
+ Io(io::Error),
43
+ }
44
+
45
+ // ===== impl Error =====
46
+
47
+ impl Error {
48
+ /// If the error was caused by the remote peer, the error reason.
49
+ ///
50
+ /// This is either an error received by the peer or caused by an invalid
51
+ /// action taken by the peer (i.e. a protocol error).
52
+ pub fn reason(&self) -> Option<Reason> {
53
+ match self.kind {
54
+ Kind::Reset(_, reason, _) | Kind::GoAway(_, reason, _) | Kind::Reason(reason) => {
55
+ Some(reason)
56
+ }
57
+ _ => None,
58
+ }
59
+ }
60
+
61
+ /// Returns true if the error is an io::Error
62
+ pub fn is_io(&self) -> bool {
63
+ matches!(self.kind, Kind::Io(..))
64
+ }
65
+
66
+ /// Returns the error if the error is an io::Error
67
+ pub fn get_io(&self) -> Option<&io::Error> {
68
+ match self.kind {
69
+ Kind::Io(ref e) => Some(e),
70
+ _ => None,
71
+ }
72
+ }
73
+
74
+ /// Returns the error if the error is an io::Error
75
+ pub fn into_io(self) -> Option<io::Error> {
76
+ match self.kind {
77
+ Kind::Io(e) => Some(e),
78
+ _ => None,
79
+ }
80
+ }
81
+
82
+ pub fn from_io(err: io::Error) -> Self {
83
+ Error {
84
+ kind: Kind::Io(err),
85
+ }
86
+ }
87
+
88
+ /// Returns true if the error is from a `GOAWAY`.
89
+ pub fn is_go_away(&self) -> bool {
90
+ matches!(self.kind, Kind::GoAway(..))
91
+ }
92
+
93
+ /// Returns true if the error is from a `RST_STREAM`.
94
+ pub fn is_reset(&self) -> bool {
95
+ matches!(self.kind, Kind::Reset(..))
96
+ }
97
+
98
+ /// Returns true if the error was received in a frame from the remote.
99
+ ///
100
+ /// Such as from a received `RST_STREAM` or `GOAWAY` frame.
101
+ pub fn is_remote(&self) -> bool {
102
+ matches!(
103
+ self.kind,
104
+ Kind::GoAway(_, _, Initiator::Remote) | Kind::Reset(_, _, Initiator::Remote)
105
+ )
106
+ }
107
+
108
+ /// Returns true if the error was created by `h2`.
109
+ ///
110
+ /// Such as noticing some protocol error and sending a GOAWAY or RST_STREAM.
111
+ pub fn is_library(&self) -> bool {
112
+ matches!(
113
+ self.kind,
114
+ Kind::GoAway(_, _, Initiator::Library) | Kind::Reset(_, _, Initiator::Library)
115
+ )
116
+ }
117
+ }
118
+
119
+ impl From<proto::Error> for Error {
120
+ fn from(src: proto::Error) -> Error {
121
+ use crate::proto::Error::*;
122
+
123
+ Error {
124
+ kind: match src {
125
+ Reset(stream_id, reason, initiator) => Kind::Reset(stream_id, reason, initiator),
126
+ GoAway(debug_data, reason, initiator) => {
127
+ Kind::GoAway(debug_data, reason, initiator)
128
+ }
129
+ Io(kind, inner) => {
130
+ Kind::Io(inner.map_or_else(|| kind.into(), |inner| io::Error::new(kind, inner)))
131
+ }
132
+ },
133
+ }
134
+ }
135
+ }
136
+
137
+ impl From<Reason> for Error {
138
+ fn from(src: Reason) -> Error {
139
+ Error {
140
+ kind: Kind::Reason(src),
141
+ }
142
+ }
143
+ }
144
+
145
+ impl From<SendError> for Error {
146
+ fn from(src: SendError) -> Error {
147
+ match src {
148
+ SendError::User(e) => e.into(),
149
+ SendError::Connection(e) => e.into(),
150
+ }
151
+ }
152
+ }
153
+
154
+ impl From<UserError> for Error {
155
+ fn from(src: UserError) -> Error {
156
+ Error {
157
+ kind: Kind::User(src),
158
+ }
159
+ }
160
+ }
161
+
162
+ impl fmt::Display for Error {
163
+ fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
164
+ let debug_data = match self.kind {
165
+ Kind::Reset(_, reason, Initiator::User) => {
166
+ return write!(fmt, "stream error sent by user: {}", reason)
167
+ }
168
+ Kind::Reset(_, reason, Initiator::Library) => {
169
+ return write!(fmt, "stream error detected: {}", reason)
170
+ }
171
+ Kind::Reset(_, reason, Initiator::Remote) => {
172
+ return write!(fmt, "stream error received: {}", reason)
173
+ }
174
+ Kind::GoAway(ref debug_data, reason, Initiator::User) => {
175
+ write!(fmt, "connection error sent by user: {}", reason)?;
176
+ debug_data
177
+ }
178
+ Kind::GoAway(ref debug_data, reason, Initiator::Library) => {
179
+ write!(fmt, "connection error detected: {}", reason)?;
180
+ debug_data
181
+ }
182
+ Kind::GoAway(ref debug_data, reason, Initiator::Remote) => {
183
+ write!(fmt, "connection error received: {}", reason)?;
184
+ debug_data
185
+ }
186
+ Kind::Reason(reason) => return write!(fmt, "protocol error: {}", reason),
187
+ Kind::User(ref e) => return write!(fmt, "user error: {}", e),
188
+ Kind::Io(ref e) => return e.fmt(fmt),
189
+ };
190
+
191
+ if !debug_data.is_empty() {
192
+ write!(fmt, " ({:?})", debug_data)?;
193
+ }
194
+
195
+ Ok(())
196
+ }
197
+ }
198
+
199
+ impl error::Error for Error {}
200
+
201
+ #[cfg(test)]
202
+ mod tests {
203
+ use super::Error;
204
+ use crate::Reason;
205
+
206
+ #[test]
207
+ fn error_from_reason() {
208
+ let err = Error::from(Reason::HTTP_1_1_REQUIRED);
209
+ assert_eq!(err.reason(), Some(Reason::HTTP_1_1_REQUIRED));
210
+ }
211
+ }
@@ -3,10 +3,13 @@
3
3
  //! with `scripts/vendor-h2.sh`.
4
4
  //!
5
5
  //! Only h2's **synchronous, sans-IO** modules are vendored: `frame` (HTTP/2
6
- //! frame parse/serialize) and `hpack` (HPACK header compression), plus the tiny
7
- //! `ext::Protocol` type they depend on. h2's async `codec`, `proto`, `client`
8
- //! and `server` are intentionally *not* vendored — that orchestration is
9
- //! rewritten in Python.
6
+ //! frame parse/serialize) and `hpack` (HPACK header compression), the tiny
7
+ //! `ext::Protocol` type they depend on, the stream state machine + flow control
8
+ //! (`proto::streams::{state,flow_control}`), the error types (`proto::error`,
9
+ //! `codec::error`, the public `error`). h2's async `codec`, the streams store
10
+ //! and orchestration, `client` and `server` are intentionally *not* vendored —
11
+ //! their state is re-expressed in httpunk's `src/h2/conn.rs`, their async half
12
+ //! in Python.
10
13
  //!
11
14
  //! The `.rs` files under here are kept **byte-identical to upstream modulo a
12
15
  //! uniform `crate::` -> `crate::` rewrite** the vendor script applies, so
@@ -33,7 +36,12 @@ macro_rules! proto_err {
33
36
  }
34
37
 
35
38
  pub mod codec;
39
+ pub mod error;
36
40
  pub mod ext;
37
41
  pub mod frame;
38
42
  pub mod hpack;
39
43
  pub mod proto;
44
+
45
+ // h2 lib.rs re-exports its public `Error` at the crate root; `proto/streams/state.rs`
46
+ // names it as `crate::Error` (`ensure_reason`).
47
+ pub use crate::error::Error;