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 h2 and http/1.1 in ALPN; the negotiated protocol decides framing.
  • h2c upgrade — an HTTP/1.1 client that sends Upgrade: h2c with an HTTP2-Settings header is switched after a 101 Switching Protocols response.
  • 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:

FeatureHow 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.
  • WebSocket handshakes still require HTTP/1.1 (RFC 8441 extended CONNECT is 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 H2SpecHost example under examples/ is the fixture used by the test suite.

Further reading