HTTP/2
Suave ships with an in-tree HTTP/2 implementation (Suave.Http2) that
speaks the binary framing layer, HPACK header compression, stream multiplexing,
flow control, prioritization, server push and trailers as defined in
RFC 7540
/ RFC 9113.
The same WebPart serves both HTTP/1.1 and HTTP/2 clients. Suave
picks the protocol automatically from the connection:
- TLS / ALPN — the server advertises
h2andhttp/1.1in ALPN; the negotiated protocol decides framing. - h2c upgrade — an HTTP/1.1 client that sends
Upgrade: h2cwith anHTTP2-Settingsheader is switched after a101 Switching Protocolsresponse. - h2c prior knowledge — a client that opens the
connection with the HTTP/2 preface
(
PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n) is picked up immediately, no HTTP/1.1 negotiation required.
Zero-config setup
HTTP/2 support is wired in at TCP-listener startup. No additional configuration is required: any web application that runs on Suave can be reached over HTTP/2. Plain HTTPS bindings get ALPN negotiation for free; plain HTTP bindings accept both h2c upgrade and h2c prior-knowledge clients.
open Suave
open Suave.Filters
open Suave.Operators
open Suave.Successful
let app =
choose [
GET >=> path "/" >=> OK "hello over HTTP/2 (or 1.1)"
]
[<EntryPoint>]
let main _ =
startWebServer defaultConfig app
0
Test the same server over both protocols:
# HTTP/1.1
curl -v http://localhost:8080/
# h2c prior knowledge
curl --http2-prior-knowledge -v http://localhost:8080/
# h2c upgrade
curl --http2 -v http://localhost:8080/
HTTP/2 over TLS (ALPN)
Browsers only speak HTTP/2 over TLS. Suave's TLS transport advertises
h2 and http/1.1 via ALPN, and the negotiated value
drives framing. Bind a certificate and you are done:
open System.Net
open System.Security.Cryptography.X509Certificates
open Suave
let cert : X509Certificate2 = (* load your certificate here *)
let cfg =
{ defaultConfig with
bindings =
[ HttpBinding.create HTTP IPAddress.Loopback 8080us
HttpBinding.create (HTTPS cert) IPAddress.Loopback 8443us ] }
startWebServer cfg app
Verify the negotiated protocol in any HTTP/2 client (Chrome / Firefox DevTools
→ Network → Protocol column, or curl -v):
curl -k --http2 -v https://localhost:8443/
nghttp -nv https://localhost:8443/
h2load -n 100 -c 1 -m 10 https://localhost:8443/
Server Push
Use Suave.Http2.Push.push to record a push intent on the response.
On an HTTP/2 connection the writer emits a PUSH_PROMISE on the
parent stream and delivers the synthesised response. On an HTTP/1.1 connection
the push intents are silently ignored, so the same handler works for both.
open Suave
open Suave.Filters
open Suave.Operators
open Suave.Successful
open Suave.Writers
let indexHtml = """<!doctype html>
<link rel="stylesheet" href="/style.css">
<script src="/app.js" defer></script>
<h1>Hello HTTP/2</h1>"""
let index : WebPart =
Http2.Push.push "/style.css" [ "accept", "text/css" ]
>=> Http2.Push.push "/app.js" [ "accept", "application/javascript" ]
>=> setMimeType "text/html; charset=utf-8"
>=> OK indexHtml
let app =
choose [
GET >=> choose [
path "/" >=> index
path "/style.css" >=> setMimeType "text/css"
>=> OK "body { font-family: system-ui; }"
path "/app.js" >=> setMimeType "application/javascript"
>=> OK "console.log('hydrated')"
]
]
Pushing respects the peer's SETTINGS_ENABLE_PUSH and
SETTINGS_MAX_CONCURRENT_STREAMS. Repeated calls to
push append — identical paths are not deduplicated.
Trailers
Trailers are headers that follow the body. They are emitted as a trailing
HEADERS block with END_STREAM set. Suave validates
each trailer against RFC 7540 §8.1.2 (no pseudo-headers, no
connection-specific fields, only te: trailers for TE);
a violation is reported as a stream-level PROTOCOL_ERROR.
open Suave
open Suave.Filters
open Suave.Operators
open Suave.Successful
let withTrailers : WebPart =
Http2.Trailers.set "x-request-duration-ms" "12"
>=> Http2.Trailers.set "x-cache" "MISS"
>=> OK "body delivered before trailers"
// Read trailers sent by the client (RFC 7230 message trailers carried over h2):
let echoTrailers : WebPart =
fun ctx ->
let received = Http2.Trailers.getRequest ctx
let body =
received
|> List.map (fun (n, v) -> sprintf "%s: %s" n v)
|> String.concat "\n"
OK body ctx
let app =
choose [
GET >=> path "/with-trailers" >=> withTrailers
POST >=> path "/echo-trailers" >=> echoTrailers
]
Settings & flow control
Suave exchanges a SETTINGS frame on connection start. The defaults
match RFC 7540 (header table 4096, initial window 65535, max frame size
16384, push enabled). The peer's settings drive flow control end-to-end:
responses larger than the initial window block until the client issues
WINDOW_UPDATE frames.
The Suave.Http2 module exposes the settings record and helpers
(defaultSetting, setInitialWindowSize,
setMaxFrameSize, setMaxConcurrentStreams, …)
for tests, tools and advanced wiring. They are not normally needed in a
web application.
What you observe on the wire
The runnable Http2Demo example exercises every feature in one place:
| Feature | How to observe it |
|---|---|
| Multiplexing | A page that references many sub-resources is fetched on one TCP
connection with concurrent streams (nghttp -nv shows
interleaved HEADERS / DATA frames). |
| HPACK | Repeated headers (content-type, server,
date) compress to one or two bytes after warm-up. |
| Prioritization | A slow handler does not block fast handlers — frame ordering is driven by stream readiness, not request arrival. |
| Flow control | A response larger than 64 KiB triggers WINDOW_UPDATE
frames from the client. |
| Server push | PUSH_PROMISE frames precede the synthesised pushed
responses on the parent stream. |
Running the demo
dotnet run --project examples/Http2Demo/Http2Demo.fsproj -- \
--http-port 8080 --https-port 8443
# h2c (clear text)
curl --http2-prior-knowledge -v http://127.0.0.1:8080/
nghttp -nv http://127.0.0.1:8080/
# TLS + ALPN (self-signed cert, regenerated each start)
curl -k --http2 -v https://127.0.0.1:8443/
h2load -n 100 -c 1 -m 10 https://127.0.0.1:8443/tile/1
Notes & caveats
- Browsers refuse to speak HTTP/2 over plain TCP — use TLS (or a terminating proxy) to exercise the protocol from a browser.
- HTTP/2 server push has been disabled in major browsers; it is still useful for non-browser clients and for h2spec conformance.
WebSockethandshakes still require HTTP/1.1 (RFC 8441 extendedCONNECTis not currently supported). Suave continues to serve WebSocket endpoints over HTTP/1.1 on the same port without any extra configuration.- The HTTP/2 implementation is verified against
h2spec;
the
H2SpecHostexample underexamples/is the fixture used by the test suite.
Further reading
- RFC 9113 — HTTP/2 (current spec, obsoletes RFC 7540)
- RFC 7541 — HPACK header compression
- Source:
src/Suave/Http2.fs - Example:
examples/Http2Demo