Skip to content

Commit 2caef2b

Browse files
whummerclaude
andcommitted
fix(miniflare): monkey-patch TwistedGateway to disable HTTP/2 via ALPN
When the `h2` Python package is installed, Twisted's Site.acceptableProtocols() advertises h2 first. ALPN then negotiates HTTP/2 for HTTPS connections, but LocalStack's WSGI-based gateway pipeline is incompatible with HTTP/2 frames, causing requests to fall through to the S3 legacy catch-all (NoSuchBucket). Override TwistedGateway.acceptableProtocols() to return only [b"http/1.1"] until HTTP/2 is properly supported upstream in rolo/localstack-core. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d06119d commit 2caef2b

1 file changed

Lines changed: 65 additions & 0 deletions

File tree

miniflare/miniflare/extension.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,74 @@
3131
WRANGLER_VERSION = "3.1.0"
3232

3333

34+
def _patch_tls_disable_http2():
35+
"""
36+
Monkey-patch LocalStack's Twisted gateway to stop advertising HTTP/2 (h2) via ALPN,
37+
so that all HTTPS connections always negotiate HTTP/1.1.
38+
39+
ROOT CAUSE
40+
----------
41+
twisted.web.server.Site.acceptableProtocols() returns [b"h2", b"http/1.1"] whenever the
42+
`h2` Python package is installed (H2_ENABLED = True in twisted.web.http). LocalStack's
43+
TwistedGateway inherits from Site, so it also advertises h2.
44+
45+
During TLS connection setup, TLSMemoryBIOFactory._createConnection() calls
46+
_applyProtocolNegotiation(), which reads wrappedFactory.acceptableProtocols() and
47+
installs an ALPN select callback that prefers the first listed protocol. Because h2 is
48+
first, any TLS client that sends h2 in its ALPN extension (modern browsers, Node.js
49+
fetch/undici, httpx with http2=True, etc.) will have h2 selected.
50+
51+
After ALPN selects h2, Twisted's _GenericHTTPChannelProtocol.dataReceived() detects
52+
`negotiatedProtocol == b"h2"` and swaps the underlying channel for an H2Connection
53+
(twisted.web.http2.H2Connection). H2Connection handles raw HTTP/2 frames and produces
54+
Request objects via Site.requestFactory, but LocalStack's gateway pipeline is built
55+
around rolo's WsgiGateway → WSGI environ → LocalstackAwsGateway. The H2Connection
56+
request/response lifecycle (streams, flow control, DATA frames) is incompatible with
57+
WSGI, so HTTP/2 requests are processed incorrectly.
58+
59+
The symptom is that HTTPS requests to extension paths (e.g. /miniflare/user) appear
60+
to return NoSuchBucket from S3 or are silently dropped, because the garbled HTTP/2
61+
frames fail to match any registered route and fall through to the legacy_s3_rules
62+
catch-all in localstack-core/localstack/aws/protocol/service_router.py:
63+
if method in ["GET", "HEAD"] and stripped:
64+
return ServiceModelIdentifier("s3") # incredibly greedy fallback
65+
66+
HOW THIS PATCH FIXES IT
67+
-----------------------
68+
We override TwistedGateway.acceptableProtocols() to return only [b"http/1.1"].
69+
This propagates through _applyProtocolNegotiation() so the ALPN select callback
70+
never picks h2. Clients then use HTTP/1.1 over TLS, which works correctly end-to-end
71+
with LocalStack's WSGI-based gateway.
72+
73+
TODO: remove this patch once HTTP/2 is properly supported in LocalStack's Twisted
74+
serving stack. The fix belongs upstream in rolo (TwistedGateway) or localstack-core
75+
(TLSMultiplexer / TwistedRuntimeServer). Proper HTTP/2 support would require
76+
integrating H2Connection's stream-based request lifecycle with rolo's gateway model,
77+
likely via an ASGI-style adapter rather than WSGI.
78+
See: https://github.com/localstack/localstack-extensions/issues (track upstream fix here)
79+
"""
80+
try:
81+
from rolo.serving.twisted import TwistedGateway
82+
83+
if getattr(TwistedGateway, "_http2_disabled_by_patch", False):
84+
return
85+
86+
def _http11_only_protocols(self):
87+
return [b"http/1.1"]
88+
89+
TwistedGateway.acceptableProtocols = _http11_only_protocols
90+
TwistedGateway._http2_disabled_by_patch = True
91+
LOG.debug("Applied TLS ALPN patch: disabled h2 advertisement for HTTPS connections")
92+
except Exception as e:
93+
LOG.warning("Could not apply TLS ALPN patch for HTTPS routing fix: %s", e)
94+
95+
3496
class MiniflareExtension(Extension):
3597
name = "miniflare"
3698

99+
def on_extension_load(self):
100+
_patch_tls_disable_http2()
101+
37102
def update_gateway_routes(self, router: http.Router[http.RouteHandler]):
38103
from miniflare.config import HANDLER_PATH_MINIFLARE
39104

0 commit comments

Comments
 (0)