horse-provider-nghttp2

September 19, 2026 · View on GitHub

Status: v1.9.5 — production-ready (h2c + TLS + mTLS + gRPC + streaming + WebSocket). Delphi 10.4+, FPC 3.2.2 and trunk 3.3.1 — gRPC needs trunk.

HTTP/2-native transport provider for Horse, built on Delphi-nghttp2 and the C library libnghttp2. Drop-in replacement for the default Indy transport — activate with one compiler define, keep your existing routes and middleware unchanged.

Companion to horse-provider-crosssocket (HTTP/1.1 async via Delphi-Cross-Socket) and horse-provider-mormot (mORMot2 stack, incl. http.sys).


What it enables

  • HTTP/2 multiplexing — clients send many concurrent streams over one TCP connection; no Head-of-Line blocking
  • HPACK header compression — reduces per-request overhead on high-frequency routes
  • TLS with ALPN — h2 over HTTPS; OpenSSL 3.x / 1.1.x auto-detected at runtime, no recompile
  • mTLS — client certificate verification for zero-trust service-to-service calls
  • gRPC — unary plus all three streaming shapes (server, client, bidirectional); protobuf codec with repeated fields; two registration styles (RegisterMethod / RegisterService<T>)
  • Async worker pool — handlers run off the connection thread; 18.3× throughput on blocking routes
  • Streaming & SSE — Res.SendStream for Web Streams (NDJSON) and Server-Sent Events; no chunked framing needed on HTTP/2
  • WebSocket over HTTP/2 — RFC 8441 extended CONNECT, sharing the connection with regular streams instead of monopolising a socket; opt-in, off by default
  • Graceful shutdown — two-stage GOAWAY per RFC 9113 §6.8; in-flight requests complete before the server closes
  • Event-loop I/O — epoll (Linux) and IOCP (Windows) engines, opt-in via UseEventLoop
  • Cross-platform — Windows/Delphi 12, Linux/FPC 3.2.2 and trunk 3.3.1, Linux/Delphi (PAServer)
  • Cross-product app shapes — Console, VCL, Daemon, Windows Service, FPC Daemon, LCL, HTTPApplication

Before you choose it

The server speaks HTTP/2 only — there is no HTTP/1.1 fallback. Your routes and middleware port over unchanged, but the wire does not:

  • HTTP/1.1-only clients are refused, not downgraded.
  • Browsers need TLS + ALPN to reach it; cleartext h2c is for native clients.
  • A reverse proxy must speak HTTP/2 on its back leg. nginx grpc_pass and Apache mod_proxy_http2 do; proxy_pass, mod_proxy_http and IIS ARR do not. See doc/deployment.md.
  • WebSocket needs RFC 8441 (extended CONNECT), not the HTTP/1.1 handshake. Opt-in and off by default. Browsers negotiate it transparently — new WebSocket(...) is unchanged — but most non-browser clients and libraries speak RFC 6455 over HTTP/1.1 only, and there is no HTTP/1.1 here to fall back to, so for them it is "cannot connect" rather than "slower path". See doc/websocket.md.

Best fit: gRPC, service-to-service APIs, and clients you control. For a public HTTP/1.1 endpoint, use one of Horse's other transports.


Quick start

Requirements

  • Delphi 10.4 Sydney or later / FPC 3.2.2 or trunk 3.3.1 — gRPC needs trunk; build 3.2.2 with -dHORSE_NGHTTP2_NO_GRPC (see doc/fpc-lazarus.md)
  • Horse ≥ 3.3.5 — stock, unpatched. Earlier versions are not enough: on 3.3.4 and below, WebSocket and streaming fail silently (see Horse core requirements)
  • Delphi-nghttp2 ≥ 1.20.0 — this is the floor boss.json declares, and it is a correctness floor rather than an API one. Below 1.20.0, PushStreamData reset the read cursor in the streaming buffer to 0 after every append (CL3b), causing ReadResponseBodyCallback to re-read from the start of the buffer on every pump — the buffer grew without bound and the worker deadlocked permanently under slow-client conditions. Any streaming or SSE route is affected; the failure is silent on the server side and surfaces as a hung connection on the client. Boss resolves the newest version satisfying the floor, so there is nothing to bump when the library releases.
  • libnghttp2 ≥ 1.59 — required at run time, dynamic-loaded (nghttp2.dll / libnghttp2.so.14 / libnghttp2.dylib); see getting-nghttp2-windows.md / getting-nghttp2-linux.md
  • OpenSSL 3.x or 1.1 for TLS only (auto-detected at runtime)

Full per-platform shipping list: doc/deployment.md.

Install with Boss:

boss install github.com/freitasjca/horse-provider-nghttp2

Horse core requirements

Nothing to do beyond using Horse ≥ 3.3.5. Everything this provider needs is upstream and released — no fork, no branch, no patched files.

boss install github.com/HashLoad/horse

This section used to tell you to clone a nghttp2-required branch carrying five patched core files. All five are now in stock Horse, verified at the published 3.3.5 artefact by each fix's own identifier rather than by its PR being marked merged:

Horse core fileProvidesLanded in
Horse.pasthe HORSE_PROVIDER_NGHTTP2 selector and its mutual-exclusion guards — without it the define does not select this providerPR #555
Horse.Response.pasadds HORSE_PROVIDER_NGHTTP2 to the stream-writer factory guardPR #552
Horse.Provider.Socket.WebSocket.pasepoll transport no longer treats EAGAIN as a disconnectPR #549
Horse.Request.pasSetWebSocketUpgrade, so RFC 8441 extended CONNECT is recognised as a WebSocketPR #550
Horse.Core.WebSocket.pasFPC-only FeedBytes interface-to-class castPR #551

Why the floor is 3.3.5 and not 3.3.0. On an older Horse the provider still compiles, and the failures are silent — which is the reason to state a version rather than let people discover it:

  • Before #552, every streaming and SSE request returns nothing — no headers, no body, no error, no log line. FStreamWriterFactory is a last-writer-wins class var set from two unit initialization sections, so which one survives depends on the compiler's dependency walk. On FPC trunk it happened to resolve correctly; on FPC 3.2.2 it does not.
  • Before #549 / #551, the RFC 8441 WebSocket handshake completes and then the connection simply stops carrying frames.
  • Before #566 (3.3.5), Windows/FPC enabled keep-alive while TCP_NODELAY was still {$IFDEF UNIX}-only, giving ~200 ms stalls on reused connections. Linux was unaffected, so this one hides from a Linux-only CI.

Activation

{$DEFINE HORSE_PROVIDER_NGHTTP2}

Legacy alias {$DEFINE HORSE_NGHTTP2} also accepted. Mutually exclusive with all other HORSE_PROVIDER_* and HORSE_HOST_* defines — enforced at compile time by FATAL guards in Horse.pas.

Minimal server

program MyHttp2Server;
{$APPTYPE CONSOLE}
{$DEFINE HORSE_PROVIDER_NGHTTP2}
uses Horse;

procedure GetPing(Req: THorseRequest; Res: THorseResponse);
begin
  Res.Send('pong');
end;

begin
  THorse.Get('/ping', GetPing);
  THorse.Listen(9200);
end.
curl --http2-prior-knowledge http://localhost:9200/ping

--http2-prior-knowledge tells curl to speak HTTP/2 immediately (no HTTP/1.1 upgrade round-trip). This is the connection mode used in h2c configuration.

Minimal client

program MyHttp2Client;
{$APPTYPE CONSOLE}
uses
  Nghttp2.Client, System.SysUtils;

var
  C: TNghttp2Client;
  R: TNghttp2Response;
begin
  C := TNghttp2Client.Create;
  try
    C.Connect('127.0.0.1', 9200);
    R := C.SubmitRequest('GET', '/ping', nil, nil);
    WriteLn('Status: ', R.Status);
    WriteLn(TEncoding.UTF8.GetString(R.Body));
  finally
    C.Free;
  end;
end.

TNghttp2Client is provided by Delphi-nghttp2. No Horse dependency on the client side — add Delphi-nghttp2/src/ to the search path and link nghttp2.dll / libnghttp2.so.14.

For TLS, pass the cert paths before Connect:

C.SSLEnabled  := True;
C.SSLCertFile := 'tls/cert.pem';   // mTLS only — omit for plain TLS
C.SSLKeyFile  := 'tls/key.pem';
C.Connect('127.0.0.1', 9443);

Documentation

TopicGuide
Roadmapdoc/roadmap.md
Concurrency & worker pooldoc/concurrency.md
Deployment — proxies, app types, load balancersdoc/deployment.md
Streaming & SSEdoc/streaming.md
WebSocket (RFC 8441)doc/websocket.md
Graceful shutdowndoc/graceful-shutdown.md
TLS and mTLSdoc/tls.md
gRPCdoc/grpc.md
Testing & benchmarkingdoc/testing.md
Platform coveragedoc/platform-coverage.md
FPC / Lazarusdoc/fpc-lazarus.md
Architecture & contributingdoc/architecture.md
Limitationsdoc/limitations.md

License

MIT. See LICENSE.