fpy3 is a high-performance asynchronous HTTP/3 (QUIC) server for Python 3.12+, built on MsQuic, nghttp3 and uvloop.
- HTTP/3 and QUIC - native support via
MsQuicandnghttp3 - HTTP/1.1 Fallback - automatic support for older browsers
- Alt-Svc Discovery - automatic upgrade to HTTP/3
- ASGI 3.0 - full compatibility via
ASGIServer - High Performance - critical paths implemented in C
- Python 3.12+
- GCC/Clang, CMake, Ninja, Autotools, pkg-config
- OpenSSL dev headers
# 1. Build dependencies (MsQuic, nghttp3)
./scripts/build_deps.sh
# 2. Install package
pip install .Libraries libmsquic.so.2 and libnghttp3.so.9 are bundled into the package automatically.
cd examples/docker_https
# Generate certificates (see "Certificates" section)
docker-compose up --buildTLS certificates are required for HTTPS/QUIC.
# Install mkcert (Ubuntu/Debian)
sudo apt install libnss3-tools
curl -JLO "https://dl.filippo.io/mkcert/latest?for=linux/amd64"
chmod +x mkcert-v*-linux-amd64
sudo mv mkcert-v*-linux-amd64 /usr/local/bin/mkcert
# Create local CA
mkcert -install
# Generate certificates
mkcert -key-file key.pem -cert-file cert.pem localhost 127.0.0.1 ::1Use certbot or an ACME client to obtain real certificates.
File hello_world.py:
import asyncio
import uvloop
from fpy3.asgi import ASGIServer
async def app(scope, receive, send):
if scope['type'] == 'http':
body = f"Hello World from FPY over HTTP/{scope['http_version']}!".encode()
await send({
'type': 'http.response.start',
'status': 200,
'headers': [(b'content-type', b'text/plain'), (b'server', b'fpy3-asgi')]
})
await send({
'type': 'http.response.body',
'body': body
})
async def main():
server = ASGIServer(app, debug=True)
server.start("0.0.0.0", 8080)
await asyncio.Event().wait()
if __name__ == "__main__":
uvloop.install()
asyncio.run(main())export LD_LIBRARY_PATH=$(pwd)/vendor/dist/lib
python3.12 hello_world.py --debug --host 0.0.0.0 --port 8080# Check content
curl -k https://127.0.0.1:8080/
# Check headers (including Alt-Svc)
curl -k -I https://127.0.0.1:8080/Expected output:
HTTP/1.1 200 OK
Alt-Svc: h3=":8080"; ma=3600
Content-Length: 40
content-type: text/plain
export LD_LIBRARY_PATH=$(pwd)/vendor/dist/lib
python3.12 test_http3_client.py# Close all Chrome windows
pkill -9 chrome
# Launch with flag
google-chrome \
--user-data-dir=/tmp/chrome_quic_test \
--origin-to-force-quic-on=127.0.0.1:8080 \
--ignore-certificate-errors \
https://127.0.0.1:8080In DevTools (F12) -> Network -> "Protocol" column should show h3.
For operation without flags, the browser must trust the certificate:
-
Find the path to Root CA:
mkcert -CAROOT
-
Import into Chrome:
chrome://settings/certificates-> Authorities -> Import- Select
rootCA.pemfrom the path above - Enable "Trust for websites"
-
Restart Chrome and open
https://127.0.0.1:8080/
cd examples/docker_https
# Generate certificates
mkcert -key-file key.pem -cert-file cert.pem localhost 127.0.0.1
docker-compose up --buildcd examples/docker_traefik_nip
# Generate wildcard certificate
mkcert -key-file key.pem -cert-file cert.pem "*.127.0.0.1.nip.io" localhost 127.0.0.1
docker-compose up --buildOpen https://app.127.0.0.1.nip.io/
+------------------+
Client (HTTP/1.1) ->| TCP Listener |-> ASGI App -> HTTP/1.1 Response + Alt-Svc
| (asyncio ssl) |
+------------------+
|
v (Alt-Svc upgrade)
+------------------+
Client (HTTP/3) ->| QUIC Listener |-> ASGI App -> HTTP/3 Response
| (MsQuic) |
+------------------+
# Make sure dependencies are built
./scripts/build_deps.sh
# Reinstall package
pip install --force-reinstall .Browser does not trust the certificate. Import Root CA into the browser (see "HTTP/3 Chrome with Alt-Svc discovery" section).
pkill -f hello_world.pyfrom fpy3.asgi import ASGIServer
server = ASGIServer(app, loop=None, debug=False)
server.start(host, port)app- ASGI 3.0 callableloop- asyncio event loop (optional)debug- enable debug logs
{
'type': 'http',
'asgi': {'version': '3.0', 'spec_version': '2.3'},
'http_version': '3' | '1.1',
'server': (host, port),
'client': (host, port),
'scheme': 'https',
'method': 'GET' | 'POST' | ...,
'path': '/...',
'query_string': b'...',
'headers': [(name, value), ...]
}