tigrcorn-protocols 0.3.16.dev5__tar.gz → 0.3.16.dev11__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 (87) hide show
  1. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/PKG-INFO +76 -19
  2. tigrcorn_protocols-0.3.16.dev11/README.md +101 -0
  3. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/pyproject.toml +9 -7
  4. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/http2/handler.py +217 -21
  5. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/http2/state.py +2 -0
  6. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/http3/handler/core.py +136 -1
  7. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/http3/handler/webtransport.py +104 -17
  8. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols.egg-info/PKG-INFO +76 -19
  9. tigrcorn_protocols-0.3.16.dev11/src/tigrcorn_protocols.egg-info/requires.txt +5 -0
  10. tigrcorn_protocols-0.3.16.dev5/README.md +0 -46
  11. tigrcorn_protocols-0.3.16.dev5/src/tigrcorn_protocols.egg-info/requires.txt +0 -5
  12. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/LICENSE +0 -0
  13. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/setup.cfg +0 -0
  14. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/__init__.py +0 -0
  15. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/_compression.py +0 -0
  16. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/connect.py +0 -0
  17. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/content_coding.py +0 -0
  18. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/custom/__init__.py +0 -0
  19. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/custom/adapters.py +0 -0
  20. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/custom/registry.py +0 -0
  21. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/flow/__init__.py +0 -0
  22. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/flow/backpressure.py +0 -0
  23. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/flow/buffers.py +0 -0
  24. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/flow/credits.py +0 -0
  25. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/flow/keepalive.py +0 -0
  26. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/flow/timeouts.py +0 -0
  27. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/flow/watermarks.py +0 -0
  28. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/http1/__init__.py +0 -0
  29. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/http1/keepalive.py +0 -0
  30. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/http1/parser.py +0 -0
  31. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/http1/serializer.py +0 -0
  32. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/http1/state.py +0 -0
  33. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/http2/__init__.py +0 -0
  34. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/http2/codec.py +0 -0
  35. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/http2/flow.py +0 -0
  36. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/http2/hpack.py +0 -0
  37. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/http2/streams.py +0 -0
  38. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/http2/websocket.py +0 -0
  39. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/http3/__init__.py +0 -0
  40. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/http3/codec.py +0 -0
  41. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/http3/handler/__init__.py +0 -0
  42. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/http3/handler.py +0 -0
  43. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/http3/qpack.py +0 -0
  44. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/http3/state.py +0 -0
  45. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/http3/streams.py +0 -0
  46. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/http3/websocket.py +0 -0
  47. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/lifespan/__init__.py +0 -0
  48. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/lifespan/driver.py +0 -0
  49. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/py.typed +0 -0
  50. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/rawframed/__init__.py +0 -0
  51. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/rawframed/codec.py +0 -0
  52. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/rawframed/frames.py +0 -0
  53. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/rawframed/handler.py +0 -0
  54. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/rawframed/state.py +0 -0
  55. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/registry.py +0 -0
  56. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/scheduler/__init__.py +0 -0
  57. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/scheduler/cancellation.py +0 -0
  58. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/scheduler/dispatch.py +0 -0
  59. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/scheduler/fairness.py +0 -0
  60. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/scheduler/policy.py +0 -0
  61. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/scheduler/priorities.py +0 -0
  62. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/scheduler/quotas.py +0 -0
  63. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/scheduler/runtime.py +0 -0
  64. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/scheduler/tasks.py +0 -0
  65. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/sessions/__init__.py +0 -0
  66. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/sessions/base.py +0 -0
  67. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/sessions/connection.py +0 -0
  68. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/sessions/limits.py +0 -0
  69. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/sessions/manager.py +0 -0
  70. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/sessions/metadata.py +0 -0
  71. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/sessions/quic.py +0 -0
  72. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/streams/__init__.py +0 -0
  73. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/streams/base.py +0 -0
  74. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/streams/ids.py +0 -0
  75. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/streams/multiplex.py +0 -0
  76. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/streams/registry.py +0 -0
  77. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/streams/singleplex.py +0 -0
  78. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/websocket/__init__.py +0 -0
  79. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/websocket/codec.py +0 -0
  80. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/websocket/extensions.py +0 -0
  81. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/websocket/frames.py +0 -0
  82. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/websocket/handler.py +0 -0
  83. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/websocket/handshake.py +0 -0
  84. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols/websocket/state.py +0 -0
  85. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols.egg-info/SOURCES.txt +0 -0
  86. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols.egg-info/dependency_links.txt +0 -0
  87. {tigrcorn_protocols-0.3.16.dev5 → tigrcorn_protocols-0.3.16.dev11}/src/tigrcorn_protocols.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tigrcorn-protocols
3
- Version: 0.3.16.dev5
3
+ Version: 0.3.16.dev11
4
4
  Summary: Protocol handlers for Tigrcorn HTTP/1.1, HTTP/2, HTTP/3, QUIC, WebSocket, WebTransport, lifespan, and ASGI3 traffic.
5
5
  Author-email: Jacob Stewart <jacob@swarmauri.com>
6
6
  License: Apache License
@@ -175,66 +175,123 @@ Classifier: License :: OSI Approved :: Apache Software License
175
175
  Classifier: Operating System :: OS Independent
176
176
  Classifier: Programming Language :: Python :: 3
177
177
  Classifier: Programming Language :: Python :: 3 :: Only
178
+ Classifier: Programming Language :: Python :: 3.10
178
179
  Classifier: Programming Language :: Python :: 3.11
179
180
  Classifier: Programming Language :: Python :: 3.12
180
181
  Classifier: Programming Language :: Python :: 3.13
182
+ Classifier: Programming Language :: Python :: 3.14
181
183
  Classifier: Topic :: Internet :: WWW/HTTP
182
184
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
183
185
  Classifier: Topic :: System :: Networking
184
186
  Classifier: Typing :: Typed
185
- Requires-Python: >=3.11
187
+ Requires-Python: <3.15,>=3.10
186
188
  Description-Content-Type: text/markdown
187
189
  License-File: LICENSE
188
- Requires-Dist: tigrcorn-core==0.3.16.dev5
189
- Requires-Dist: tigrcorn-config==0.3.16.dev5
190
- Requires-Dist: tigrcorn-asgi==0.3.16.dev5
191
- Requires-Dist: tigrcorn-http==0.3.16.dev5
192
- Requires-Dist: tigrcorn-transports==0.3.16.dev5
190
+ Requires-Dist: tigrcorn-core==0.3.16.dev11
191
+ Requires-Dist: tigrcorn-config==0.3.16.dev11
192
+ Requires-Dist: tigrcorn-asgi==0.3.16.dev11
193
+ Requires-Dist: tigrcorn-http==0.3.16.dev11
194
+ Requires-Dist: tigrcorn-transports==0.3.16.dev11
193
195
  Dynamic: license-file
194
196
 
195
197
  <div align="center">
196
198
  <h1>tigrcorn-protocols</h1>
199
+ <img
200
+ src="https://raw.githubusercontent.com/Tigrbl/tigrcorn/master/assets/tigrcorn_logo.png"
201
+ alt="Tigrcorn tiger-unicorn logo"
202
+ width="140"
203
+ />
197
204
 
198
205
  <p><strong>Protocol handlers for Tigrcorn HTTP/1.1, HTTP/2, HTTP/3, QUIC, WebSocket, WebTransport, lifespan, and ASGI3 traffic.</strong></p>
199
206
 
200
207
  <a href="https://pypi.org/project/tigrcorn-protocols/"><img alt="PyPI version for tigrcorn-protocols" src="https://img.shields.io/pypi/v/tigrcorn-protocols?label=PyPI"></a>
201
208
  <a href="https://pypi.org/project/tigrcorn-protocols/"><img alt="tigrcorn-protocols package on PyPI" src="https://img.shields.io/badge/package-PyPI-blue"></a>
209
+ <a href="https://pepy.tech/project/tigrcorn-protocols"><img alt="Downloads for tigrcorn-protocols" src="https://static.pepy.tech/badge/tigrcorn-protocols"></a>
210
+ <a href="https://github.com/tigrbl/tigrcorn/blob/master/pkgs/tigrcorn-protocols/README.md"><img alt="Hits for tigrcorn-protocols README" src="https://hits.sh/github.com/tigrbl/tigrcorn/blob/master/pkgs/tigrcorn-protocols/README.md.svg?label=hits"></a>
202
211
  <a href="LICENSE"><img alt="Apache 2.0 license" src="https://img.shields.io/badge/license-Apache%202.0-525252"></a>
203
- <a href="pyproject.toml"><img alt="Python 3.11 supported" src="https://img.shields.io/badge/python-3.11-3776ab"></a>
204
- <a href="pyproject.toml"><img alt="Python 3.12 supported" src="https://img.shields.io/badge/python-3.12-3776ab"></a>
205
- <a href="pyproject.toml"><img alt="Python 3.13 supported" src="https://img.shields.io/badge/python-3.13-3776ab"></a>
206
- <a href="src/tigrcorn_protocols/py.typed"><img alt="typed package" src="https://img.shields.io/badge/typed-py.typed-2f7ed8"></a>
212
+ <a href="pyproject.toml"><img alt="Python 3.10 | 3.11 | 3.12 | 3.13 | 3.14 supported" src="https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-3776ab"></a>
207
213
  <a href="https://pypi.org/project/tigrcorn-protocols/"><img alt="protocols role package" src="https://img.shields.io/badge/role-protocols-0a7f5a"></a>
208
214
  </div>
209
215
 
216
+ <p align="center"><a href="https://github.com/Tigrbl/tigrcorn/blob/master/.ssot/registry.json"><img alt="SSOT governed" src="https://img.shields.io/badge/SSOT-governed-2f6f4e.svg"></a> <a href="https://discord.gg/jzvrbEtTtt"><img alt="Discord" src="https://img.shields.io/badge/Discord-Join%20chat-5865F2?logo=discord&amp;logoColor=white"></a></p>
217
+
210
218
  ## Install
211
219
 
212
- ~~~bash
220
+ ```bash
221
+ uv add tigrcorn-protocols
222
+ ```
223
+
224
+ ```bash
213
225
  pip install tigrcorn-protocols
214
- ~~~
226
+ ```
215
227
 
216
228
  Use the aggregate [tigrcorn](https://pypi.org/project/tigrcorn/) distribution when you want the full ASGI3 Python web server stack. Install <code>tigrcorn-protocols</code> directly when you want only this package boundary and its declared dependencies.
217
229
 
218
230
  ## What It Owns
219
231
 
220
- <code>tigrcorn-protocols</code> owns HTTP/1.1, HTTP/2, HTTP/3, WebSocket, lifespan, rawframed, custom protocols, flow control, scheduler primitives, sessions, and streams. Its import package is <code>tigrcorn_protocols</code>, and its declared package dependencies are: tigrcorn-core, tigrcorn-config, tigrcorn-asgi, tigrcorn-http, tigrcorn-transports.
232
+ <code>tigrcorn-protocols</code> owns http1, http2, http3, websocket, lifespan, rawframed, custom protocols, flow control, scheduler primitives, sessions, and streams. Its import package is <code>tigrcorn_protocols</code>, and its declared package dependencies are: tigrcorn-core, tigrcorn-config, tigrcorn-asgi, tigrcorn-http, tigrcorn-transports.
233
+
234
+ This package page is written for developers searching for Tigrcorn ASGI3 server components, Python web server packages, HTTP/3 and QUIC support, WebSocket and WebTransport-adjacent surfaces, and Apache 2.0 licensed infrastructure.
235
+
236
+ ## Why Use This?
237
+
238
+ Use <code>tigrcorn-protocols</code> when you want the protocols layer as a direct install target instead of the full server bundle. It lets application, operator, or certification workflows depend on this boundary explicitly while keeping the broader Tigrcorn runtime assembled from smaller repo-owned package surfaces.
239
+
240
+ ## FAQ
241
+
242
+ ### What does this package export?
243
+
244
+ The package exports through the <code>tigrcorn_protocols</code> namespace and keeps the root <code>tigrcorn</code> package as the compatibility umbrella.
245
+
246
+ ### Which boundary does this package own?
247
+
248
+ It is the package boundary for http1, http2, http3, websocket, lifespan, rawframed, custom protocols, flow control, scheduler primitives, sessions, and streams in the Tigrcorn package graph.
221
249
 
222
- This package page is written for developers searching for Tigrcorn ASGI3 server components, Python web server packages, HTTP/3 and QUIC support, WebSocket and WebTransport runtime surfaces, typed package boundaries, and Apache 2.0 licensed infrastructure.
250
+ ### What protocol families are implemented here?
251
+
252
+ This package is the main protocol plane for HTTP/1.1, HTTP/2, HTTP/3, WebSocket, WebTransport-adjacent protocol logic, lifespan, rawframed, scheduler primitives, sessions, and streams.
253
+
254
+ ## Features
255
+
256
+ - Owns http1, http2, http3, websocket, lifespan, rawframed, custom protocols, flow control, scheduler primitives, sessions, and streams inside the Tigrcorn split-package architecture.
257
+ - Publishes the <code>tigrcorn_protocols</code> import surface for module-oriented public surfaces.
258
+ - Declared runtime dependencies: tigrcorn-core, tigrcorn-config, tigrcorn-asgi, tigrcorn-http, tigrcorn-transports.
259
+ - Optional dependency surface: none.
260
+ - Supports Python 3.10, 3.11, 3.12, 3.13, and 3.14.
223
261
 
224
262
  ## Use It When
225
263
 
226
- Use <code>tigrcorn-protocols</code> when you need protocol-level Tigrcorn handling for ASGI3 traffic, backpressure, flow control, HTTP/3, QUIC, WebSocket, or WebTransport behavior. It is part of Tigrcorn's split-package architecture, so it can be installed independently while remaining linked to the rest of the Tigrcorn package family on PyPI.
264
+ Use <code>tigrcorn-protocols</code> when you need protocols-level behavior without pulling the entire server stack into the import surface. It is part of Tigrcorn's split-package architecture, so it can be installed independently while remaining linked to the rest of the Tigrcorn package family on PyPI.
227
265
 
228
266
  ## Import Surface
229
267
 
230
- ~~~python
268
+ ```python
231
269
  import tigrcorn_protocols
232
270
 
233
271
  print(tigrcorn_protocols.__name__)
234
- ~~~
272
+ ```
235
273
 
236
274
  The package exposes its supported public surface through the <code>tigrcorn_protocols</code> namespace. The root [tigrcorn](https://pypi.org/project/tigrcorn/) package keeps compatibility shims for users who install the full server distribution.
237
275
 
276
+ ## Related Packages
277
+
278
+ - [tigrcorn-core](https://pypi.org/project/tigrcorn-core/)
279
+ - [tigrcorn-config](https://pypi.org/project/tigrcorn-config/)
280
+ - [tigrcorn-asgi](https://pypi.org/project/tigrcorn-asgi/)
281
+ - [tigrcorn-http](https://pypi.org/project/tigrcorn-http/)
282
+ - [tigrcorn-transports](https://pypi.org/project/tigrcorn-transports/)
283
+ - [tigrcorn](https://pypi.org/project/tigrcorn/)
284
+
238
285
  ## Package Graph
239
286
 
240
- [tigrcorn](https://pypi.org/project/tigrcorn/) | [tigrcorn-core](https://pypi.org/project/tigrcorn-core/) | [tigrcorn-config](https://pypi.org/project/tigrcorn-config/) | [tigrcorn-asgi](https://pypi.org/project/tigrcorn-asgi/) | [tigrcorn-contract](https://pypi.org/project/tigrcorn-contract/) | [tigrcorn-transports](https://pypi.org/project/tigrcorn-transports/) | [tigrcorn-protocols](https://pypi.org/project/tigrcorn-protocols/) | [tigrcorn-http](https://pypi.org/project/tigrcorn-http/) | [tigrcorn-security](https://pypi.org/project/tigrcorn-security/) | [tigrcorn-runtime](https://pypi.org/project/tigrcorn-runtime/) | [tigrcorn-static](https://pypi.org/project/tigrcorn-static/) | [tigrcorn-observability](https://pypi.org/project/tigrcorn-observability/) | [tigrcorn-compat](https://pypi.org/project/tigrcorn-compat/) | [tigrcorn-certification](https://pypi.org/project/tigrcorn-certification/)
287
+ [tigrcorn-core](https://pypi.org/project/tigrcorn-core/) | [tigrcorn-config](https://pypi.org/project/tigrcorn-config/) | [tigrcorn-http](https://pypi.org/project/tigrcorn-http/) | [tigrcorn-asgi](https://pypi.org/project/tigrcorn-asgi/) | [tigrcorn-contract](https://pypi.org/project/tigrcorn-contract/) | [tigrcorn-transports](https://pypi.org/project/tigrcorn-transports/) | [tigrcorn-security](https://pypi.org/project/tigrcorn-security/) | [tigrcorn-protocols](https://pypi.org/project/tigrcorn-protocols/) | [tigrcorn-static](https://pypi.org/project/tigrcorn-static/) | [tigrcorn-observability](https://pypi.org/project/tigrcorn-observability/) | [tigrcorn-runtime](https://pypi.org/project/tigrcorn-runtime/) | [tigrcorn-compat](https://pypi.org/project/tigrcorn-compat/) | [tigrcorn-certification](https://pypi.org/project/tigrcorn-certification/)
288
+
289
+ ## Best Practices
290
+
291
+ - Use this package for protocol-state machines and stream/session behavior, not for transport bootstrapping.
292
+ - Keep HTTP, WebSocket, and HTTP/3 behavior aligned with the certified protocol surface.
293
+ - Validate new protocol work against shared ASGI and HTTP helper layers before widening runtime claims.
294
+
295
+ ## License
296
+
297
+ Apache-2.0
@@ -0,0 +1,101 @@
1
+ <div align="center">
2
+ <h1>tigrcorn-protocols</h1>
3
+ <img
4
+ src="https://raw.githubusercontent.com/Tigrbl/tigrcorn/master/assets/tigrcorn_logo.png"
5
+ alt="Tigrcorn tiger-unicorn logo"
6
+ width="140"
7
+ />
8
+
9
+ <p><strong>Protocol handlers for Tigrcorn HTTP/1.1, HTTP/2, HTTP/3, QUIC, WebSocket, WebTransport, lifespan, and ASGI3 traffic.</strong></p>
10
+
11
+ <a href="https://pypi.org/project/tigrcorn-protocols/"><img alt="PyPI version for tigrcorn-protocols" src="https://img.shields.io/pypi/v/tigrcorn-protocols?label=PyPI"></a>
12
+ <a href="https://pypi.org/project/tigrcorn-protocols/"><img alt="tigrcorn-protocols package on PyPI" src="https://img.shields.io/badge/package-PyPI-blue"></a>
13
+ <a href="https://pepy.tech/project/tigrcorn-protocols"><img alt="Downloads for tigrcorn-protocols" src="https://static.pepy.tech/badge/tigrcorn-protocols"></a>
14
+ <a href="https://github.com/tigrbl/tigrcorn/blob/master/pkgs/tigrcorn-protocols/README.md"><img alt="Hits for tigrcorn-protocols README" src="https://hits.sh/github.com/tigrbl/tigrcorn/blob/master/pkgs/tigrcorn-protocols/README.md.svg?label=hits"></a>
15
+ <a href="LICENSE"><img alt="Apache 2.0 license" src="https://img.shields.io/badge/license-Apache%202.0-525252"></a>
16
+ <a href="pyproject.toml"><img alt="Python 3.10 | 3.11 | 3.12 | 3.13 | 3.14 supported" src="https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-3776ab"></a>
17
+ <a href="https://pypi.org/project/tigrcorn-protocols/"><img alt="protocols role package" src="https://img.shields.io/badge/role-protocols-0a7f5a"></a>
18
+ </div>
19
+
20
+ <p align="center"><a href="https://github.com/Tigrbl/tigrcorn/blob/master/.ssot/registry.json"><img alt="SSOT governed" src="https://img.shields.io/badge/SSOT-governed-2f6f4e.svg"></a> <a href="https://discord.gg/jzvrbEtTtt"><img alt="Discord" src="https://img.shields.io/badge/Discord-Join%20chat-5865F2?logo=discord&amp;logoColor=white"></a></p>
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ uv add tigrcorn-protocols
26
+ ```
27
+
28
+ ```bash
29
+ pip install tigrcorn-protocols
30
+ ```
31
+
32
+ Use the aggregate [tigrcorn](https://pypi.org/project/tigrcorn/) distribution when you want the full ASGI3 Python web server stack. Install <code>tigrcorn-protocols</code> directly when you want only this package boundary and its declared dependencies.
33
+
34
+ ## What It Owns
35
+
36
+ <code>tigrcorn-protocols</code> owns http1, http2, http3, websocket, lifespan, rawframed, custom protocols, flow control, scheduler primitives, sessions, and streams. Its import package is <code>tigrcorn_protocols</code>, and its declared package dependencies are: tigrcorn-core, tigrcorn-config, tigrcorn-asgi, tigrcorn-http, tigrcorn-transports.
37
+
38
+ This package page is written for developers searching for Tigrcorn ASGI3 server components, Python web server packages, HTTP/3 and QUIC support, WebSocket and WebTransport-adjacent surfaces, and Apache 2.0 licensed infrastructure.
39
+
40
+ ## Why Use This?
41
+
42
+ Use <code>tigrcorn-protocols</code> when you want the protocols layer as a direct install target instead of the full server bundle. It lets application, operator, or certification workflows depend on this boundary explicitly while keeping the broader Tigrcorn runtime assembled from smaller repo-owned package surfaces.
43
+
44
+ ## FAQ
45
+
46
+ ### What does this package export?
47
+
48
+ The package exports through the <code>tigrcorn_protocols</code> namespace and keeps the root <code>tigrcorn</code> package as the compatibility umbrella.
49
+
50
+ ### Which boundary does this package own?
51
+
52
+ It is the package boundary for http1, http2, http3, websocket, lifespan, rawframed, custom protocols, flow control, scheduler primitives, sessions, and streams in the Tigrcorn package graph.
53
+
54
+ ### What protocol families are implemented here?
55
+
56
+ This package is the main protocol plane for HTTP/1.1, HTTP/2, HTTP/3, WebSocket, WebTransport-adjacent protocol logic, lifespan, rawframed, scheduler primitives, sessions, and streams.
57
+
58
+ ## Features
59
+
60
+ - Owns http1, http2, http3, websocket, lifespan, rawframed, custom protocols, flow control, scheduler primitives, sessions, and streams inside the Tigrcorn split-package architecture.
61
+ - Publishes the <code>tigrcorn_protocols</code> import surface for module-oriented public surfaces.
62
+ - Declared runtime dependencies: tigrcorn-core, tigrcorn-config, tigrcorn-asgi, tigrcorn-http, tigrcorn-transports.
63
+ - Optional dependency surface: none.
64
+ - Supports Python 3.10, 3.11, 3.12, 3.13, and 3.14.
65
+
66
+ ## Use It When
67
+
68
+ Use <code>tigrcorn-protocols</code> when you need protocols-level behavior without pulling the entire server stack into the import surface. It is part of Tigrcorn's split-package architecture, so it can be installed independently while remaining linked to the rest of the Tigrcorn package family on PyPI.
69
+
70
+ ## Import Surface
71
+
72
+ ```python
73
+ import tigrcorn_protocols
74
+
75
+ print(tigrcorn_protocols.__name__)
76
+ ```
77
+
78
+ The package exposes its supported public surface through the <code>tigrcorn_protocols</code> namespace. The root [tigrcorn](https://pypi.org/project/tigrcorn/) package keeps compatibility shims for users who install the full server distribution.
79
+
80
+ ## Related Packages
81
+
82
+ - [tigrcorn-core](https://pypi.org/project/tigrcorn-core/)
83
+ - [tigrcorn-config](https://pypi.org/project/tigrcorn-config/)
84
+ - [tigrcorn-asgi](https://pypi.org/project/tigrcorn-asgi/)
85
+ - [tigrcorn-http](https://pypi.org/project/tigrcorn-http/)
86
+ - [tigrcorn-transports](https://pypi.org/project/tigrcorn-transports/)
87
+ - [tigrcorn](https://pypi.org/project/tigrcorn/)
88
+
89
+ ## Package Graph
90
+
91
+ [tigrcorn-core](https://pypi.org/project/tigrcorn-core/) | [tigrcorn-config](https://pypi.org/project/tigrcorn-config/) | [tigrcorn-http](https://pypi.org/project/tigrcorn-http/) | [tigrcorn-asgi](https://pypi.org/project/tigrcorn-asgi/) | [tigrcorn-contract](https://pypi.org/project/tigrcorn-contract/) | [tigrcorn-transports](https://pypi.org/project/tigrcorn-transports/) | [tigrcorn-security](https://pypi.org/project/tigrcorn-security/) | [tigrcorn-protocols](https://pypi.org/project/tigrcorn-protocols/) | [tigrcorn-static](https://pypi.org/project/tigrcorn-static/) | [tigrcorn-observability](https://pypi.org/project/tigrcorn-observability/) | [tigrcorn-runtime](https://pypi.org/project/tigrcorn-runtime/) | [tigrcorn-compat](https://pypi.org/project/tigrcorn-compat/) | [tigrcorn-certification](https://pypi.org/project/tigrcorn-certification/)
92
+
93
+ ## Best Practices
94
+
95
+ - Use this package for protocol-state machines and stream/session behavior, not for transport bootstrapping.
96
+ - Keep HTTP, WebSocket, and HTTP/3 behavior aligned with the certified protocol surface.
97
+ - Validate new protocol work against shared ASGI and HTTP helper layers before widening runtime claims.
98
+
99
+ ## License
100
+
101
+ Apache-2.0
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tigrcorn-protocols"
7
- version = "0.3.16.dev5"
7
+ version = "0.3.16.dev11"
8
8
  description = "Protocol handlers for Tigrcorn HTTP/1.1, HTTP/2, HTTP/3, QUIC, WebSocket, WebTransport, lifespan, and ASGI3 traffic."
9
9
  readme = "README.md"
10
- requires-python = ">=3.11"
10
+ requires-python = ">=3.10,<3.15"
11
11
  license = {file = "LICENSE"}
12
12
  authors = [{name = "Jacob Stewart", email = "jacob@swarmauri.com"}]
13
13
  keywords = ["tigrcorn", "http1", "http2", "http3", "websocket", "webtransport", "quic", "asgi3", "protocol-handlers"]
@@ -19,20 +19,22 @@ classifiers = [
19
19
  "Operating System :: OS Independent",
20
20
  "Programming Language :: Python :: 3",
21
21
  "Programming Language :: Python :: 3 :: Only",
22
+ "Programming Language :: Python :: 3.10",
22
23
  "Programming Language :: Python :: 3.11",
23
24
  "Programming Language :: Python :: 3.12",
24
25
  "Programming Language :: Python :: 3.13",
26
+ "Programming Language :: Python :: 3.14",
25
27
  "Topic :: Internet :: WWW/HTTP",
26
28
  "Topic :: Software Development :: Libraries :: Python Modules",
27
29
  "Topic :: System :: Networking",
28
30
  "Typing :: Typed",
29
31
  ]
30
32
  dependencies = [
31
- "tigrcorn-core==0.3.16.dev5",
32
- "tigrcorn-config==0.3.16.dev5",
33
- "tigrcorn-asgi==0.3.16.dev5",
34
- "tigrcorn-http==0.3.16.dev5",
35
- "tigrcorn-transports==0.3.16.dev5",
33
+ "tigrcorn-core==0.3.16.dev11",
34
+ "tigrcorn-config==0.3.16.dev11",
35
+ "tigrcorn-asgi==0.3.16.dev11",
36
+ "tigrcorn-http==0.3.16.dev11",
37
+ "tigrcorn-transports==0.3.16.dev11",
36
38
  ]
37
39
 
38
40
  [tool.setuptools]
@@ -4,7 +4,7 @@ import asyncio
4
4
  from contextlib import suppress
5
5
  from urllib.parse import urlsplit
6
6
 
7
- from tigrcorn_asgi.receive import HTTPRequestReceive, apply_request_trailer_policy
7
+ from tigrcorn_asgi.receive import HTTP2QueuedRequestReceive, HTTPRequestReceive, apply_request_trailer_policy
8
8
  from tigrcorn_asgi.scopes.http import build_http_scope
9
9
  from tigrcorn_asgi.send import HTTPResponseCollector, iter_response_body_segments, response_body_segments_have_bytes
10
10
  from tigrcorn_config.model import ServerConfig
@@ -384,6 +384,7 @@ class HTTP2ConnectionHandler:
384
384
  for name, value in headers:
385
385
  if any(65 <= byte <= 90 for byte in name):
386
386
  raise ProtocolError("uppercase header field name forbidden in HTTP/2")
387
+ self._validate_field_value(value)
387
388
  if name.startswith(b":"):
388
389
  raise ProtocolError("trailer pseudo-header forbidden in HTTP/2")
389
390
  if name in {b"connection", b"upgrade", b"proxy-connection", b"transfer-encoding"}:
@@ -391,6 +392,36 @@ class HTTP2ConnectionHandler:
391
392
  if name == b"te" and value.lower() != b"trailers":
392
393
  raise ProtocolError("invalid TE header for HTTP/2")
393
394
 
395
+ def _validate_field_value(self, value: bytes) -> None:
396
+ if any(byte in {0x00, 0x0A, 0x0D} for byte in value):
397
+ raise ProtocolError("invalid HTTP/2 header field value")
398
+ if value[:1] in {b" ", b"\t"} or value[-1:] in {b" ", b"\t"}:
399
+ raise ProtocolError("invalid HTTP/2 header field value")
400
+
401
+ def _parse_content_length(self, headers: list[tuple[bytes, bytes]]) -> int | None:
402
+ values: list[bytes] = []
403
+ for name, value in headers:
404
+ if name.lower() != b"content-length":
405
+ continue
406
+ for part in value.split(b","):
407
+ parsed = part.strip()
408
+ if not parsed:
409
+ raise ProtocolError("invalid content-length header")
410
+ values.append(parsed)
411
+ if not values:
412
+ return None
413
+ parsed_value: int | None = None
414
+ for value in values:
415
+ if not value.isdigit():
416
+ raise ProtocolError("invalid content-length header")
417
+ current = int(value)
418
+ if parsed_value is None:
419
+ parsed_value = current
420
+ continue
421
+ if parsed_value != current:
422
+ raise ProtocolError("conflicting content-length values")
423
+ return parsed_value
424
+
394
425
  async def _handle_headers(self, frame: HTTP2Frame) -> None:
395
426
  if frame.stream_id == 0:
396
427
  raise ProtocolError("HEADERS must use a stream id")
@@ -540,10 +571,19 @@ class HTTP2ConnectionHandler:
540
571
  elif payload:
541
572
  if state.buffered_body_size + len(payload) > self.config.max_body_size:
542
573
  raise ProtocolError("request body exceeds configured max_body_size")
543
- state.append_body(payload)
544
- await self._maybe_replenish_receive_credit(frame.stream_id, len(payload))
574
+ if state.request_receive is not None:
575
+ await state.request_receive.put_body(payload, more_body=not bool(frame.flags & FLAG_END_STREAM))
576
+ else:
577
+ state.append_body(payload)
578
+ if (
579
+ state.expected_content_length is not None
580
+ and state.buffered_body_size > state.expected_content_length
581
+ ):
582
+ raise ProtocolError("request body exceeds content-length")
545
583
  if frame.flags & FLAG_END_STREAM:
546
584
  state.receive_end_stream()
585
+ if state.request_receive is not None and not payload:
586
+ await state.request_receive.finish_body()
547
587
  await self._maybe_dispatch(frame.stream_id)
548
588
  self._finalize_stream_if_complete(frame.stream_id)
549
589
 
@@ -558,6 +598,7 @@ class HTTP2ConnectionHandler:
558
598
  else:
559
599
  state.headers = headers
560
600
  state.headers_complete = True
601
+ state.expected_content_length = self._parse_content_length(headers)
561
602
  state.header_fragments.clear()
562
603
  state.header_block_bytes = 0
563
604
  state.awaiting_continuation = False
@@ -567,9 +608,31 @@ class HTTP2ConnectionHandler:
567
608
  state = self.streams.find(stream_id)
568
609
  if state is None or state.dispatched or not state.headers_complete:
569
610
  return
611
+ protocol = self._extended_connect_protocol(state.headers)
570
612
  is_ws = self._is_extended_connect_websocket(state.headers)
571
613
  is_connect = self._is_generic_connect_tunnel(state.headers)
572
- if not is_ws and not is_connect and not state.end_stream_received:
614
+ if protocol is not None and (protocol != b"websocket" or not self.config.websocket.enabled):
615
+ request = self._build_request(state)
616
+ if not self._admit_stream_work(stream_id):
617
+ await self._send_response(stream_id, 503, [(b"content-type", b"text/plain")], b"scheduler overloaded")
618
+ self.access_logger.log_http(self.client, request.method, request.path, 503, "HTTP/2")
619
+ self._release_stream_work_lease(stream_id)
620
+ self._cancel_stream(stream_id)
621
+ self.streams.close(stream_id)
622
+ self._maybe_finish_after_goaway()
623
+ return
624
+ state.dispatched = True
625
+ await self._send_response(
626
+ stream_id,
627
+ 501,
628
+ [(b"content-type", b"text/plain")],
629
+ b"unsupported extended connect protocol",
630
+ )
631
+ self.access_logger.log_http(self.client, request.method, request.path, 501, "HTTP/2")
632
+ self._release_stream_work_lease(stream_id)
633
+ self._cancel_stream(stream_id)
634
+ self.streams.close(stream_id)
635
+ self._maybe_finish_after_goaway()
573
636
  return
574
637
  if not self._admit_stream_work(stream_id):
575
638
  request = self._build_request(state)
@@ -673,13 +736,18 @@ class HTTP2ConnectionHandler:
673
736
  def _pseudo_headers(self, headers: list[tuple[bytes, bytes]]) -> dict[bytes, bytes]:
674
737
  return {k: v for k, v in headers if k.startswith(b":")}
675
738
 
676
- def _is_extended_connect_websocket(self, headers: list[tuple[bytes, bytes]]) -> bool:
739
+ def _extended_connect_protocol(self, headers: list[tuple[bytes, bytes]]) -> bytes | None:
677
740
  pseudo = self._pseudo_headers(headers)
678
- return pseudo.get(b":method") == b"CONNECT" and pseudo.get(b":protocol") == b"websocket"
741
+ if pseudo.get(b":method") != b"CONNECT":
742
+ return None
743
+ return pseudo.get(b":protocol")
744
+
745
+ def _is_extended_connect_websocket(self, headers: list[tuple[bytes, bytes]]) -> bool:
746
+ return self._extended_connect_protocol(headers) == b"websocket"
679
747
 
680
748
  def _is_generic_connect_tunnel(self, headers: list[tuple[bytes, bytes]]) -> bool:
681
749
  pseudo = self._pseudo_headers(headers)
682
- return pseudo.get(b":method") == b"CONNECT" and pseudo.get(b":protocol") is None
750
+ return pseudo.get(b":method") == b"CONNECT" and self._extended_connect_protocol(headers) is None
683
751
  def _release_stream_work_lease(self, stream_id: int) -> None:
684
752
  lease = self.stream_work_leases.pop(stream_id, None)
685
753
  if lease is not None:
@@ -735,6 +803,8 @@ class HTTP2ConnectionHandler:
735
803
  method_text = method.decode("ascii", "strict").upper()
736
804
  else:
737
805
  method_text = str(method).upper()
806
+ if method_text not in {"GET", "HEAD"}:
807
+ raise ProtocolError("HTTP/2 server push requires a safe cacheable method")
738
808
  authority = message.get("authority")
739
809
  if authority is None:
740
810
  authority_bytes = pseudo.get(b":authority", b"")
@@ -875,6 +945,105 @@ class HTTP2ConnectionHandler:
875
945
  )
876
946
  return processed.status, processed.headers, processed.body, ([] if processed.head_response else trailers), informational, None, cleanup
877
947
 
948
+ async def _run_http_app_live(self, stream_id: int, request: ParsedRequest, *, allow_push: bool) -> int:
949
+ extensions = dict(self.scope_extensions)
950
+ state = self.streams.find(stream_id)
951
+ if state is None:
952
+ return 500
953
+ raw_request_trailers = list(state.trailers)
954
+ try:
955
+ request_trailers = apply_request_trailer_policy(raw_request_trailers, self.config.http.trailer_policy)
956
+ except ProtocolError:
957
+ await self._send_response(stream_id, 400, [(b"content-type", b"text/plain")], b"bad request trailers", [], body_segments=None)
958
+ return 400
959
+ if request.method.upper() == "CONNECT":
960
+ extensions["tigrcorn.http.connect"] = {"authority": request.target}
961
+ if request_trailers and self.config.http.trailer_policy != 'drop':
962
+ extensions["tigrcorn.http.request_trailers"] = {}
963
+ if allow_push and self.state.client_allows_push:
964
+ extensions["http.response.push"] = {}
965
+ extensions['tigrcorn.http.response.file'] = {'protocol': 'http/2', 'streaming': True, 'sendfile': False}
966
+ extensions['http.response.pathsend'] = {}
967
+ scope = build_http_scope(
968
+ request,
969
+ client=self.client,
970
+ server=self.server,
971
+ scheme=self.scheme,
972
+ extensions=extensions,
973
+ root_path=self.config.proxy.root_path,
974
+ proxy=self.config.proxy,
975
+ )
976
+ receive = HTTP2QueuedRequestReceive(
977
+ trailers=request_trailers,
978
+ trailer_policy=self.config.http.trailer_policy,
979
+ on_body_consumed=lambda amount: self._maybe_replenish_receive_credit(stream_id, amount),
980
+ )
981
+ state.request_receive = receive
982
+ for part in state.body_parts:
983
+ await receive.put_body(part, more_body=True)
984
+ state.body_parts.clear()
985
+ if state.end_stream_received:
986
+ await receive.finish_body()
987
+
988
+ response_started = False
989
+ response_complete = False
990
+ final_status = 500
991
+
992
+ async def send(message: dict) -> None:
993
+ nonlocal response_started, response_complete, final_status
994
+ message_type = message.get("type")
995
+ if message_type == "http.response.push":
996
+ if not allow_push or not self.state.client_allows_push:
997
+ raise ProtocolError("HTTP/2 server push is not available on this stream")
998
+ await self._send_push_promise(stream_id, message)
999
+ return
1000
+ if message_type == "http.response.start":
1001
+ status = int(message["status"])
1002
+ headers = list(message.get("headers", []))
1003
+ if status < 200:
1004
+ await self._send_stream_headers(stream_id, status, sanitize_early_hints_headers(headers), end_stream=False)
1005
+ return
1006
+ if response_started:
1007
+ raise ProtocolError("http.response.start sent more than once")
1008
+ response_started = True
1009
+ final_status = status
1010
+ await self._send_stream_headers(stream_id, status, headers, end_stream=False)
1011
+ return
1012
+ if message_type == "http.response.body":
1013
+ if response_complete:
1014
+ raise ProtocolError("http.response.body sent after response completion")
1015
+ if not response_started:
1016
+ response_started = True
1017
+ final_status = 200
1018
+ await self._send_stream_headers(stream_id, 200, [], end_stream=False)
1019
+ body = bytes(message.get("body", b""))
1020
+ more_body = bool(message.get("more_body", False))
1021
+ await self._send_stream_data(stream_id, body, end_stream=not more_body)
1022
+ if not more_body:
1023
+ response_complete = True
1024
+ return
1025
+ raise ProtocolError(f"unsupported HTTP/2 ASGI send message: {message_type!r}")
1026
+
1027
+ try:
1028
+ await self.app(scope, receive, send)
1029
+ if not response_complete:
1030
+ if not response_started:
1031
+ response_started = True
1032
+ final_status = 200
1033
+ await self._send_stream_headers(stream_id, 200, [], end_stream=False)
1034
+ await self._send_stream_data(stream_id, b"", end_stream=True)
1035
+ response_complete = True
1036
+ return final_status
1037
+ except Exception:
1038
+ if not response_started and self.streams.find(stream_id) is not None:
1039
+ await self._send_response(stream_id, 500, [(b"content-type", b"text/plain")], b"internal server error")
1040
+ elif response_started and not response_complete and self.streams.find(stream_id) is not None:
1041
+ await self._send_stream_data(stream_id, b"", end_stream=True)
1042
+ return 500
1043
+ finally:
1044
+ if state.request_receive is receive:
1045
+ state.request_receive = None
1046
+
878
1047
  async def _send_push_promise(self, parent_stream_id: int, message: dict) -> None:
879
1048
  if not self.state.client_allows_push:
880
1049
  return
@@ -1078,9 +1247,11 @@ class HTTP2ConnectionHandler:
1078
1247
  pseudo_seen: set[bytes] = set()
1079
1248
  regular_seen = False
1080
1249
  allowed_pseudo = {b":method", b":scheme", b":authority", b":path", b":protocol"}
1250
+ host_values: list[bytes] = []
1081
1251
  for name, value in headers:
1082
1252
  if any(65 <= byte <= 90 for byte in name):
1083
1253
  raise ProtocolError("uppercase header field name forbidden in HTTP/2")
1254
+ self._validate_field_value(value)
1084
1255
  if name.startswith(b":"):
1085
1256
  if regular_seen:
1086
1257
  raise ProtocolError("pseudo-header after regular header")
@@ -1095,10 +1266,22 @@ class HTTP2ConnectionHandler:
1095
1266
  raise ProtocolError("connection-specific header forbidden in HTTP/2")
1096
1267
  if name == b"te" and value.lower() != b"trailers":
1097
1268
  raise ProtocolError("invalid TE header for HTTP/2")
1269
+ if name == b"host":
1270
+ host_values.append(value)
1098
1271
  if b":method" not in pseudo_seen:
1099
1272
  raise ProtocolError("missing :method pseudo-header")
1100
- method = dict(headers).get(b":method", b"GET")
1101
- protocol = dict(headers).get(b":protocol")
1273
+ pseudo_headers = {name: value for name, value in headers if name.startswith(b":")}
1274
+ method = pseudo_headers.get(b":method", b"GET")
1275
+ protocol = pseudo_headers.get(b":protocol")
1276
+ authority = pseudo_headers.get(b":authority")
1277
+ if authority is not None and b"@" in authority:
1278
+ raise ProtocolError("userinfo is forbidden in :authority")
1279
+ if host_values:
1280
+ normalized_hosts = {value.lower() for value in host_values}
1281
+ if len(normalized_hosts) != 1:
1282
+ raise ProtocolError("conflicting host header values")
1283
+ if authority is not None and next(iter(normalized_hosts)) != authority.lower():
1284
+ raise ProtocolError("host header must match :authority")
1102
1285
  if protocol is not None:
1103
1286
  if method != b"CONNECT":
1104
1287
  raise ProtocolError("extended CONNECT requires CONNECT method")
@@ -1121,15 +1304,26 @@ class HTTP2ConnectionHandler:
1121
1304
  pseudo = {k: v for k, v in state.headers if k.startswith(b":")}
1122
1305
  headers = [(k, v) for k, v in state.headers if not k.startswith(b":")]
1123
1306
  method = pseudo.get(b":method", b"GET").decode("ascii", "strict")
1307
+ if state.expected_content_length is not None:
1308
+ observed = len(state.body)
1309
+ if observed > state.expected_content_length:
1310
+ raise ProtocolError("request body exceeds content-length")
1311
+ if state.end_stream_received and observed != state.expected_content_length:
1312
+ raise ProtocolError("request body does not match content-length")
1124
1313
  if method.upper() == "CONNECT" and pseudo.get(b":protocol") != b"websocket":
1125
1314
  target = pseudo.get(b":authority", b"").decode("ascii", "strict")
1126
1315
  path = target
1127
1316
  raw_path = target.encode("ascii", "strict")
1128
1317
  query_string = b""
1129
1318
  else:
1130
- target = pseudo.get(b":path", b"/").decode("ascii", "strict")
1319
+ target_bytes = pseudo.get(b":path", b"")
1320
+ if not target_bytes:
1321
+ raise ProtocolError("empty :path pseudo-header")
1322
+ target = target_bytes.decode("ascii", "strict")
1131
1323
  split = urlsplit(target)
1132
- path = split.path or "/"
1324
+ if not split.path:
1325
+ raise ProtocolError("malformed request target")
1326
+ path = split.path
1133
1327
  raw_path = path.encode("utf-8")
1134
1328
  query_string = split.query.encode("ascii")
1135
1329
  return ParsedRequest(
@@ -1162,18 +1356,20 @@ class HTTP2ConnectionHandler:
1162
1356
  self.streams.close(stream_id)
1163
1357
  self._maybe_finish_after_goaway()
1164
1358
  return
1165
- status, headers, body, trailers, informational, body_segments, cleanup = await self._run_http_app(stream_id, request, allow_push=True)
1166
- for interim_status, interim_headers in informational:
1167
- await self._send_stream_headers(stream_id, interim_status, sanitize_early_hints_headers(interim_headers), end_stream=False)
1168
- try:
1169
- await self._send_response(stream_id, status, headers, body, trailers, body_segments=body_segments)
1170
- finally:
1171
- if cleanup is not None:
1172
- cleanup()
1359
+ if state.end_stream_received:
1360
+ status, headers, body, trailers, informational, body_segments, cleanup = await self._run_http_app(stream_id, request, allow_push=True)
1361
+ for interim_status, interim_headers in informational:
1362
+ await self._send_stream_headers(stream_id, interim_status, sanitize_early_hints_headers(interim_headers), end_stream=False)
1363
+ try:
1364
+ await self._send_response(stream_id, status, headers, body, trailers, body_segments=body_segments)
1365
+ finally:
1366
+ if cleanup is not None:
1367
+ cleanup()
1368
+ else:
1369
+ status = await self._run_http_app_live(stream_id, request, allow_push=True)
1173
1370
  self.access_logger.log_http(self.client, request.method, request.path, status, "HTTP/2")
1174
1371
  if self.streams.find(stream_id) is not None:
1175
- self._cancel_stream(stream_id)
1176
- self.streams.close(stream_id)
1372
+ self._finalize_stream_if_complete(stream_id)
1177
1373
  self._maybe_finish_after_goaway()
1178
1374
  finally:
1179
1375
  self._release_stream_work_lease(stream_id)