Skip to content

Add experimental HTTP/2 support via zttp#2982

Open
Kludex wants to merge 2 commits into
add-zttp-http-protocolfrom
zttp-http2
Open

Add experimental HTTP/2 support via zttp#2982
Kludex wants to merge 2 commits into
add-zttp-http-protocolfrom
zttp-http2

Conversation

@Kludex

@Kludex Kludex commented Jun 9, 2026

Copy link
Copy Markdown
Owner

Stacked on #2979. Adds an experimental --http2 flag, with the HTTP/2 protocol implemented on top of zttp's H2Connection. 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) loads ZttpH2Protocol and offers ["h2", "http/1.1"] via ALPN on TLS.
  • Each HTTP/1.1 protocol (h11, httptools, zttp) hands the transport over to the HTTP/2 protocol when ALPN negotiated h2, or when a cleartext connection opens with the HTTP/2 preface (prior-knowledge h2c, i.e. curl --http2-prior-knowledge and proxy h2c:// upstreams).
  • One RequestResponseCycle per stream; responses are sent through zttp's per-stream Stream handles, with outbound flow control handled inside zttp.
  • Upgrade-based h2c (RFC 7540 3.2) is not supported: the request is served as HTTP/1.1, which the RFC allows.

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:

Known limitations

  • Upgrade-based h2c (RFC 7540 3.2) is served as HTTP/1.1 rather than upgraded (allowed by the RFC).
  • No server push (deprecated in practice; not planned).

AI Disclaimer

This PR was developed with the assistance of either Claude or Codex. I've reviewed and verified the changes.

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.
@Kludex Kludex marked this pull request as ready for review June 12, 2026 13:11

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread uvicorn/config.py
fd: int | None = None,
loop: LoopFactoryType | str = "auto",
http: type[asyncio.Protocol] | HTTPProtocolType | str = "auto",
http2: bool | type[asyncio.Protocol] | str = False,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Comment thread docs/concepts/http2.md

- 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant