Quickstart¶
This tutorial runs a FastAPI application under h2corn, proves both the HTTP/1.1 and cleartext h2c paths against it, and ends with the flags a production start line needs.
Install¶
uv add h2corn fastapi
pip install h2corn fastapi
h2corn supports Python 3.11 or newer. Wheels are published for Linux, macOS, and Windows, including ABI-matched free-threaded CPython 3.14t builds. The multi-worker supervisor is Unix-only — see Operations.
Run your first server¶
Save a tiny FastAPI app as hello.py:
from fastapi import FastAPI
app = FastAPI()
@app.get('/')
async def index():
return {'message': 'hello from h2corn'}
Then start h2corn, pointing it at the module and the ASGI app object inside it:
h2corn hello:app
With no address given, h2corn binds 127.0.0.1:8000 — reachable from this machine and nowhere else. Visit http://127.0.0.1:8000/ in your browser. The response is {"message": "hello from h2corn"}.
For a repeatable terminal check, leave that process running and use a second terminal:
curl --http1.1 --fail http://127.0.0.1:8000/
The request succeeds because the default http1 setting is enabled.
Why does the browser use HTTP/1.1?
Browsers do not speak cleartext h2c with prior knowledge. The development server therefore keeps HTTP/1.1 enabled for local browser testing. A cleartext browser success is HTTP/1.1, not HTTP/2. In production, HTTPS is exposed through a reverse proxy or h2corn's own Direct TLS. --no-http1 applies only when the client or proxy is configured for HTTP/2.
Prove h2c with prior knowledge¶
An HTTP/2 client must opt into cleartext prior knowledge. Use a curl build that lists HTTP/2 in curl --version:
curl --version | grep -q 'HTTP2'
curl --http2-prior-knowledge --fail http://127.0.0.1:8000/
--http2-prior-knowledge sends the HTTP/2 connection preface straight to h2corn with no Upgrade handshake, on the same listener the HTTP/1.1 request used.
Hot reload¶
While iterating locally, add --reload so h2corn restarts whenever your source files change:
h2corn hello:app --reload
The watcher follows *.py by default; tune it with --reload-include and --reload-exclude. Reload is intended for development only and cannot be combined with multiple workers.
Application factories¶
Some applications expose their ASGI object through a factory function rather than a module-level attribute. --factory makes h2corn call the target:
from fastapi import FastAPI
def create_app() -> FastAPI:
app = FastAPI()
@app.get('/')
async def index():
return {'message': 'hello from h2corn'}
return app
h2corn factory:create_app --factory
Deploy¶
In production, h2corn typically sits behind a reverse proxy that terminates browser-facing TLS. The application server itself runs on a local listener with several workers. A production start line can use:
h2corn hello:app \
--bind 127.0.0.1:8000 \
--workers 4 \
--proxy-headers \
--forwarded-allow-ips 127.0.0.1,::1,unix \
--no-http1
Production flags:
--bind127.0.0.1:8000listens on loopback only so the proxy can reach it locally. Use0.0.0.0:portfor a public-facing TCP listener.--workers4runs four worker processes; size it to the core count and the application's concurrency.--proxy-headersinterpretsX-Forwarded-ForandX-Forwarded-Protofrom peers listed in--forwarded-allow-ips. Use--forwarded-fieldsto select additional X-Forwarded fields or the RFC 7239Forwardeddialect.--no-http1rejects HTTP/1.1 connections outright, so once the upstream is configured to speakh2c, an accidental fallback fails immediately instead of serving at the lower protocol.
Swap the start line for --check-config to validate the resolved configuration and exit without importing the target or binding a listener, and --print-config to print what a start would actually use once environment, file, and CLI values have been merged.
For larger command lines, use a TOML config file with the same keys.
Migrate from another ASGI server¶
ASGI entrypoint mapping does not include options, protocol limits, lifespan, WebSockets, forwarded identity, or shutdown behavior; those differ between servers. myapp:app is the application import target.
These are the defaults most likely to change behavior after the swap, read from uvicorn 0.52.4, gunicorn 26.1.0 and hypercorn 0.18.0:
| What differs | Elsewhere | h2corn |
|---|---|---|
| Forwarded-header trust | uvicorn interprets X-Forwarded-For and X-Forwarded-Proto with no opt-in | off until --proxy-headers, then only from peers in --forwarded-allow-ips |
| HTTPS marker spelling | gunicorn also reads X-Forwarded-Ssl and X-Forwarded-Protocol | neither is recognized; the proxy must send X-Forwarded-Proto |
| Idle keep-alive | uvicorn 5 s, gunicorn 2 s, hypercorn 5 s | --timeout-keep-alive 120 s |
| Graceful stop budget | gunicorn 30 s, hypercorn 3 s, uvicorn unbounded | --timeout-graceful-shutdown 30 s |
From Uvicorn¶
uvicorn myapp:app --host 127.0.0.1 --port 8000 --workers 4
h2corn myapp:app --bind 127.0.0.1:8000 --workers 4
Use --loop asyncio or uvloop to choose the callback loop. --reload is a development-only watcher and is limited to one worker.
From Hypercorn¶
hypercorn myapp:app --bind 127.0.0.1:8000 --workers 4
h2corn myapp:app --bind 127.0.0.1:8000 --workers 4
Both serve HTTP/2. Port the remaining settings through the Configuration reference, then test the protocol path you deploy.
From Gunicorn¶
gunicorn myapp:app -k uvicorn.workers.UvicornWorker --workers 4 \
--bind 127.0.0.1:8000
h2corn myapp:app --bind 127.0.0.1:8000 --workers 4
h2corn does not load Gunicorn worker classes or Gunicorn configuration. Its worker lifecycle, signals, and resource limits are documented in Operations.
Framework boundary¶
h2corn accepts ASGI 3 callables. Django (asgi:application), Litestar, Quart, and similar frameworks work when their exported callable meets the ASGI contract; validate lifespan, WebSockets, and middleware behavior.
Next steps¶
- Behind a proxy — full Caddy and HAProxy recipes with
h2cupstream. - Direct TLS — terminate TLS in
h2cornitself for single-server deployments. - Operations — multi-worker supervisor, signals, rolling reload, live scaling, and recycling.
- Configuration — every option, in CLI, environment, and TOML form.