Add experimental HTTP/2 support via zttp#2982
Conversation
zttp 0.0.12 ships the completed HTTP/2 API the experimental --http2 flag needs: inbound flow-control replenishment, SETTINGS/PING auto-ack, GOAWAY and RST_STREAM send, and 100-continue. Bump the pin from 0.0.10, move the coverage/mypy gates to >= (0, 0, 12) so the HTTP/2 protocol module is covered, and advance the zttp exclude-newer cutoff to its release date.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5d44fff50b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # the new credit released. | ||
|
|
||
| def handle_request(self, event: zttp.Request) -> None: | ||
| path = event.path.decode("ascii") |
There was a problem hiding this comment.
Decode percent-encoded HTTP/2 paths
When an HTTP/2 request uses the normal URI form such as :path: /users/alice%20smith, this assigns scope["path"] to the encoded string rather than the decoded path. The existing HTTP/1.1 code unquotes the raw path before building the ASGI scope, and ASGI apps/routers commonly match against the decoded path, so routes containing escaped characters will behave differently or fail only under HTTP/2.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
4 issues found across 14 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="uvicorn/config.py">
<violation number="1" location="uvicorn/config.py:193">
P2: Adding `http2` in the middle of `Config.__init__` breaks backward compatibility for positional arguments by shifting `ws` and later parameters.</violation>
</file>
<file name="uvicorn/protocols/http/zttp_h2_impl.py">
<violation number="1" location="uvicorn/protocols/http/zttp_h2_impl.py:195">
P2: HTTP/2 request scope uses a non-decoded `path`, causing inconsistent routing semantics versus HTTP/1 handlers.</violation>
</file>
<file name="docs/concepts/http2.md">
<violation number="1" location="docs/concepts/http2.md:109">
P3: The HTTP/2 limitations section is stale: SETTINGS/PING handling is now delegated to zttp, so this should not be documented as an outstanding protocol violation.</violation>
</file>
<file name="tests/protocols/test_http2.py">
<violation number="1" location="tests/protocols/test_http2.py:824">
P2: Attach `caplog.handler` after `Config.load()`/protocol construction; `dictConfig()` removes the handler when the config is loaded, so this test won’t capture the trace logs.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| fd: int | None = None, | ||
| loop: LoopFactoryType | str = "auto", | ||
| http: type[asyncio.Protocol] | HTTPProtocolType | str = "auto", | ||
| http2: bool | type[asyncio.Protocol] | str = False, |
There was a problem hiding this comment.
P2: Adding http2 in the middle of Config.__init__ breaks backward compatibility for positional arguments by shifting ws and later parameters.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At uvicorn/config.py, line 193:
<comment>Adding `http2` in the middle of `Config.__init__` breaks backward compatibility for positional arguments by shifting `ws` and later parameters.</comment>
<file context>
@@ -186,6 +190,7 @@ def __init__(
fd: int | None = None,
loop: LoopFactoryType | str = "auto",
http: type[asyncio.Protocol] | HTTPProtocolType | str = "auto",
+ http2: bool | type[asyncio.Protocol] | str = False,
ws: type[asyncio.Protocol] | WSProtocolType | str = "auto",
ws_max_size: int = 16 * 1024 * 1024,
</file context>
| # the new credit released. | ||
|
|
||
| def handle_request(self, event: zttp.Request) -> None: | ||
| path = event.path.decode("ascii") |
There was a problem hiding this comment.
P2: HTTP/2 request scope uses a non-decoded path, causing inconsistent routing semantics versus HTTP/1 handlers.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At uvicorn/protocols/http/zttp_h2_impl.py, line 195:
<comment>HTTP/2 request scope uses a non-decoded `path`, causing inconsistent routing semantics versus HTTP/1 handlers.</comment>
<file context>
@@ -0,0 +1,502 @@
+ # the new credit released.
+
+ def handle_request(self, event: zttp.Request) -> None:
+ path = event.path.decode("ascii")
+ full_path = self.root_path + path
+ full_raw_path = self.root_path.encode("ascii") + event.path
</file context>
| async def test_trace_logging(caplog: pytest.LogCaptureFixture, logging_config: dict[str, Any]): | ||
| app = Response("Hello, world", media_type="text/plain") | ||
| logger = logging.getLogger("uvicorn.error") | ||
| logger.addHandler(caplog.handler) |
There was a problem hiding this comment.
P2: Attach caplog.handler after Config.load()/protocol construction; dictConfig() removes the handler when the config is loaded, so this test won’t capture the trace logs.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/protocols/test_http2.py, line 824:
<comment>Attach `caplog.handler` after `Config.load()`/protocol construction; `dictConfig()` removes the handler when the config is loaded, so this test won’t capture the trace logs.</comment>
<file context>
@@ -0,0 +1,972 @@
+async def test_trace_logging(caplog: pytest.LogCaptureFixture, logging_config: dict[str, Any]):
+ app = Response("Hello, world", media_type="text/plain")
+ logger = logging.getLogger("uvicorn.error")
+ logger.addHandler(caplog.handler)
+ try:
+ protocol = get_connected_protocol(app, log_level="trace", log_config=logging_config)
</file context>
|
|
||
| - Request bodies are limited by the HTTP/2 flow-control window (64 KiB): the server does not | ||
| yet replenish the window, so larger uploads stall. Use HTTP/1.1 for uploads for now. | ||
| - `SETTINGS` and `PING` frames from the client are not yet acknowledged, which strict clients |
There was a problem hiding this comment.
P3: The HTTP/2 limitations section is stale: SETTINGS/PING handling is now delegated to zttp, so this should not be documented as an outstanding protocol violation.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/concepts/http2.md, line 109:
<comment>The HTTP/2 limitations section is stale: SETTINGS/PING handling is now delegated to zttp, so this should not be documented as an outstanding protocol violation.</comment>
<file context>
@@ -0,0 +1,112 @@
+
+- Request bodies are limited by the HTTP/2 flow-control window (64 KiB): the server does not
+ yet replenish the window, so larger uploads stall. Use HTTP/1.1 for uploads for now.
+- `SETTINGS` and `PING` frames from the client are not yet acknowledged, which strict clients
+ may treat as a protocol violation on long-lived connections.
+- Graceful shutdown closes the connection without sending `GOAWAY`.
</file context>
Stacked on #2979. Adds an experimental
--http2flag, with the HTTP/2 protocol implemented on top ofzttp'sH2Connection. Supersedes the approach in #2793 (hyper-h2) by reusing the same Zig-backed parser the zttp HTTP/1.1 protocol uses.How it works
--http2/Config(http2=True)loadsZttpH2Protocoland offers["h2", "http/1.1"]via ALPN on TLS.h2, or when a cleartext connection opens with the HTTP/2 preface (prior-knowledge h2c, i.e.curl --http2-prior-knowledgeand proxyh2c://upstreams).RequestResponseCycleper stream; responses are sent through zttp's per-streamStreamhandles, with outbound flow control handled inside zttp.Verified end-to-end with curl over TLS ALPN (incl. POST body), prior-knowledge cleartext, multiplexed requests, and HTTP/1.1 fallback.
Requires zttp 0.0.12
The completed HTTP/2 API landed in zttp 0.0.12 (https://github.com/Kludex/zttp/releases/tag/v0.0.12). The pin and the
package_version('zttp') >= (0, 0, 12)coverage/mypy gates target it, so HTTP/2 tests and coverage activate on 3.12+ where zttp is installed.Previously blocking, now resolved in zttp 0.0.12
All the zttp-side gaps this PR was waiting on are fixed and released:
WINDOW_UPDATE, so request bodies and large responses no longer stall past 64 KiB (zttp uvicorn.run.run function shadows uvicorn.run module #60).connection.close()) and individual streams can be reset (stream.reset()→ RST_STREAM) (zttp Don't override path if it was not provided yet #59).stream.send_informational()) (zttp Version 0.2 #84).Known limitations
AI Disclaimer
This PR was developed with the assistance of either Claude or Codex. I've reviewed and verified the changes.