"""Tests confirming hypercorn serves HTTP/2 (h2c) without TLS. Wall 13: production was running uvicorn (HTTP/1.1 only). Cloudflare's gRPC proxy requires HTTP/2 end-to-end to origin. hypercorn supports h2c (HTTP/2 cleartext) which is what nginx grpc_pass sends to the backend. Test plan: A. hypercorn can be imported and is available in the environment B. hypercorn Config supports h2c (no certfile/keyfile needed) C. A live hypercorn server on a random port accepts HTTP/2 requests via httpx D. The push/stream endpoint receives a body over HTTP/2 """ from __future__ import annotations import asyncio import socket import threading import time import pytest # --------------------------------------------------------------------------- # A. hypercorn is importable # --------------------------------------------------------------------------- def test_hypercorn_is_importable(): import hypercorn # noqa: F401 def test_hypercorn_asyncio_serve_is_importable(): from hypercorn.asyncio import serve # noqa: F401 def test_hypercorn_config_is_importable(): from hypercorn.config import Config # noqa: F401 # --------------------------------------------------------------------------- # B. hypercorn Config supports h2c (no TLS required) # --------------------------------------------------------------------------- def test_hypercorn_config_h2c_no_tls(): from hypercorn.config import Config cfg = Config() cfg.bind = ["0.0.0.0:0"] # No certfile / keyfile — this is h2c (cleartext HTTP/2) assert cfg.ssl_enabled is False def test_hypercorn_config_h2_in_default_alpn(): from hypercorn.config import Config cfg = Config() # h2 must be in the ALPN protocols hypercorn advertises when TLS is present. # This ensures TLS mode also supports HTTP/2. assert "h2" in cfg.alpn_protocols # --------------------------------------------------------------------------- # C. Live h2c server accepts HTTP/2 via raw h2 socket (nginx grpc_pass style) # --------------------------------------------------------------------------- def _free_port() -> int: with socket.socket() as s: s.bind(("127.0.0.1", 0)) return s.getsockname()[1] def _h2c_get(host: str, port: int, path: str = "/") -> int: """Send an HTTP/2 h2c GET using the h2 library directly. This mirrors what nginx grpc_pass does: open a plain TCP connection, send the HTTP/2 client preface, send headers, read the response status. Returns the HTTP status code. """ import h2.connection import h2.config import h2.events sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(5) sock.connect((host, port)) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) cfg = h2.config.H2Configuration(client_side=True, header_encoding="utf-8") conn = h2.connection.H2Connection(config=cfg) conn.initiate_connection() conn.send_headers( stream_id=1, headers=[ (":method", "GET"), (":path", path), (":authority", f"{host}:{port}"), (":scheme", "http"), ], end_stream=True, ) sock.sendall(conn.data_to_send(65535)) status = None while status is None: data = sock.recv(65535) if not data: break events = conn.receive_data(data) for event in events: if isinstance(event, h2.events.ResponseReceived): for k, v in event.headers: if k == ":status": status = int(v) sock.sendall(conn.data_to_send(65535)) sock.close() return status or 0 def test_hypercorn_live_h2c_http2(): """Start a real hypercorn server (no TLS) and confirm it speaks HTTP/2 h2c. This test proves the nginx grpc_pass → hypercorn path works: nginx opens a plain TCP connection and sends the HTTP/2 preface directly (no TLS, no Upgrade). httpx uses ALPN/TLS for HTTP/2 and cannot test h2c — we use the h2 library. """ from hypercorn.asyncio import serve from hypercorn.config import Config async def app(scope, receive, send): if scope["type"] == "http": await send({"type": "http.response.start", "status": 200, "headers": []}) await send({"type": "http.response.body", "body": b"ok", "more_body": False}) port = _free_port() cfg = Config() cfg.bind = [f"127.0.0.1:{port}"] cfg.loglevel = "WARNING" import multiprocessing as _mp def _server_proc(port: int, ready_q): import asyncio as _asyncio from hypercorn.asyncio import serve as _serve from hypercorn.config import Config as _Config async def _app(scope, receive, send): if scope["type"] == "http": await send({"type": "http.response.start", "status": 200, "headers": []}) await send({"type": "http.response.body", "body": b"ok", "more_body": False}) _cfg = _Config() _cfg.bind = [f"127.0.0.1:{port}"] _cfg.loglevel = "WARNING" async def _run(): ready_q.put("ready") await _serve(_app, _cfg) _asyncio.run(_run()) ctx = _mp.get_context("fork") ready_q: _mp.Queue = ctx.Queue() proc = ctx.Process(target=_server_proc, args=(port, ready_q), daemon=True) proc.start() ready_q.get(timeout=10) time.sleep(0.15) try: status = _h2c_get("127.0.0.1", port) assert status == 200, ( f"expected HTTP 200 via h2c but got {status} — " "hypercorn must accept h2c connections for nginx grpc_pass to work" ) finally: proc.terminate() proc.join(timeout=3)