An HTTP/2 client library for Free Pascal.
The client speaks HTTP/2 (RFC 7540 and RFC 9113) and HPACK (RFC 7541). The library is written in Object Pascal. It uses OpenSSL for TLS, and it has no other C dependency.
The client multiplexes many streams on one TCP connection. One request uses one stream. The pool keeps many TCP connections.
- Uses TLS with ALPN. The peer must select
h2. - Sends and receives the HTTP/2 frames. It handles HPACK and flow control.
- Runs one request at a time on each stream. Many streams share one connection.
- Follows redirects. It has a redirect limit.
- Applies connect, header, and idle timeouts. A caller can cancel a request.
- Reports errors through an exception hierarchy. It reports events through an observer interface.
- Speaks cleartext
h2cwhen the caller enables it. Cleartext has three modes: prior knowledge, upgrade, and HTTP/1.1 with no probe. A server that declines the upgrade answers the request on the same connection, so the client sends the request once. - Falls back to HTTP/1.1 when the peer does not offer
h2. The caller must enable the fallback. - Reads Server-Sent Events (
text/event-stream) incrementally, with a per-request body read timeout, read-side cancellation, and an optional reconnect-and-resume loop. Seedoc/design/server-sent-events.md. - Opens WebSocket connections (
ws://,wss://; RFC 6455) with a listener, demand-driven delivery, fragment-aware sends and the closing handshake, directly or through any of the proxy kinds. Seedoc/design/websocket.md.
The default is strict. The client uses h2 over TLS only.
| Item | Value |
|---|---|
| Free Pascal | 3.2.4 or later. Use {$mode delphi}. |
| OpenSSL | Version 3. Set OPENSSL_LIBPATH to the library directory. |
| mORMot2 | A pinned checkout in third_party/mORMot2. |
| Task | Version 3.54 or later. This tool runs the build tasks. |
| Docker | Only for the conformance harness. The image is not on a registry. |
| nghttpd | Only for the interop gate. Install it with brew install nghttp2. |
macOS example for the OpenSSL path:
export OPENSSL_LIBPATH=/opt/homebrew/opt/openssl@3/libDo these steps one time after you clone the repository:
task setupThe setup task does three things:
- Clones the pinned
third_partycheckouts. - Installs
nghttp2whennghttpdis absent. - Builds the local Docker image
h2-test-harness.
Each step is safe to repeat. A second run does no work.
The validate tasks run the setup steps when they need them. You can therefore
start with task validate.
make also works. The Makefile runs the build and the unit suite.
task buildThe build task compiles every library unit into bin/. The output is the
.ppu and .o files for each unit. The task does not create a linked binary,
because the library has no program of its own. To use the units in your own
program, add -Fu for the src and bin directories. See the FPCFLAGS
value in Taskfile.yaml.
The umbrella unit Http2 re-exports the core library. Http2.Readers.pas
is not part of it and is optional: it adds JSON and XML helpers and is the only unit that depends on
fcl-json or fcl-xml. Name it in your uses clause only when you need it.
See Examples.
task testThe test task compiles the library and the unit suite. Then it runs the
suite. The suite has 648 tests (the SSE live case skips unless
SSE_TEST_URL is set; run it with task validate:sse).
The suite uses fpcunit and a mock socket. It needs no network and no server.
Every test must pass. The task returns a non-zero exit code when a test fails.
The validation suite has four levels, plus four opt-in live gates. Each level has its own task.
| Task | What it checks | Time |
|---|---|---|
task test |
The unit suite. | Under 1 minute |
task validate:sanity |
The probe classifies outcomes correctly. | Seconds |
task validate:interop |
A real TLS peer, nghttpd. Cases A.1 to A.13. |
About 1 minute |
task validate:harness |
The RFC conformance harness. 146 case ids. | About 30 minutes |
task validate:sse |
The opt-in live SSE case against tools/validate/sse_server.py. |
Seconds |
task validate:websocket |
The opt-in live WebSocket case: ws and wss, directly and through an HTTP CONNECT, https and SOCKS5 proxy, against tools/validate/ws_servers.py, plus a check of the servers' logs (mask bit on every client frame, close codes, route). task validate:websocket:autobahn runs the Autobahn |
Testsuite client cases (Docker). |
task validate:mtls |
The opt-in live mutual TLS case: an accepted identity, none, one from another CA and an expired one, against tools/validate/mtls_server.py (HTTP/1.1, an event stream and wss, pinned to TLS 1.2 and to TLS 1.3) and nghttpd --verify-client (HTTP/2). The expired identity is refused by the client at Build. It also runs a selector against two origins that each serve one identity, an https proxy that requires its own client certificate in front of an origin that requires another, an HTTP CONNECT proxy through which the origin still sees the origin identity, and a per-request identity. |
Seconds |
task validate:cleartext |
The opt-in live cleartext case: a bodyless GET, POST, DELETE and HEAD under ctUpgrade and ctHttp1 against tools/validate/h1_counter_server.py, an HTTP/1.1-only server that logs each request; every request arrives once. |
Seconds |
task validate:proxy |
The opt-in live proxy case: HTTP CONNECT, https and SOCKS5 proxies from tools/validate/proxy_servers.py, with and without credentials, HTTP/2 through each proxy kind when nghttpd is installed, plus a check of the proxy logs. |
Seconds |
Run the full suite with one command:
task validatetask validate runs the setup, then the four levels in order. It stops at the
first level that fails.
Run the fast subset with this command:
task validate:fasttask validate:fast runs the setup and every level except the harness sweep.
Use it during development.
The sanity script proves that the probe does not report success always.
It checks five conditions, run by task validate:sanity (or
tools/validate/h2probe-sanity.sh):
- An unreachable port gives exit code 2 (connection error).
- A reset that follows a 200 response gives exit code 3 (stream error).
The peer is a local fabricated server (
tools/validate/fake_rst_server.py) that sendsHEADERS(200)thenRST_STREAM(CANCEL)and holds the socket open, so classification is deterministic with no container or host-port race. - Bad command-line usage gives exit code 1.
- A peer that does not offer
h2over ALPN gives exit code 2. - A live
nghttpdGET gives exit code 0. This check is a SKIP whennghttpdis absent.
The script waits for each server to open its port. If a server does not start, the script reports an infrastructure failure. It does not report a probe failure. A probe failure always means that the probe is wrong.
The interop script starts nghttpd with the test certificates. Then it runs
the cases A.1 to A.13. It prints a table and exits 0 when no mandatory case
fails.
Case A.8 is a SKIP. No nghttpd trigger can force a GOAWAY in the middle of
a request. The unit suite covers the same behavior.
Cases A.11 and A.12 are SKIPs when Docker is absent. A.10–A.13 cover the cleartext and fallback paths (h2c prior knowledge, h2c upgrade, HTTP/1.1 fallback, strict-mode reject).
The harness script runs the image h2-test-harness. For each case id it does
two runs:
- It runs the harness reference client. This run declares an outcome.
- It runs our probe. This run gives our outcome.
Then the script compares the two outcomes. The verdicts are:
- MATCH. The two outcomes are equal.
- BETTER. We found an error. The reference did not.
- WORSE. The reference found an error. We did not. This is a failure.
- CLASS-DIFF. Both sides found an error. The levels are different.
- UNKNOWN. Neither side resolved the case.
The gate passes when there are zero WORSE rows.
The recorded full sweep gives this result:
| Verdict | Count |
|---|---|
| MATCH | 29 |
| BETTER | 41 |
| WORSE | 0 |
| CLASS-DIFF | 13 |
| UNKNOWN | 63 |
PASS = MATCH + BETTER = 70; FAIL = WORSE = 0.
This is a representative snapshot, not a reproducible constant: individual
rows move between runs within a few load-sensitive families, and they depend on
the client's own validation (e.g. 8.1.2.2/2 is stream-error because the
client validates response te). What is stable is the
strictness level — WORSE = 0 whenever the sweep runs alone.
The script writes the table to bin/validation-results.md; the raw per-id
log is doc/verification/harness-run-full.log.
The harness server binds host port 8080, so the sweep must run alone. A
second concurrent sweep, or any other heavy work, makes the probe hit the
wrong peer and records spurious timeout/WORSE rows. Re-check a suspect id
in isolation:
HARNESS_IDS="5.4.1/2" bash tools/validate/harness.shThis example builds a client and sends one GET request:
uses
{$IFDEF UNIX}cthreads,{$ENDIF}
SysUtils, Classes, Http2.Client;
var
Client: IHttpClient;
Response: IHttpResponse;
begin
Client := THttpClientFactory.Create
.WithMaxConnectionsPerHost(2)
.WithMaxTotalConnections(8)
.WithMaxStreamsPerConnection(50)
.WithFollowRedirects(False)
.Build;
Response := Client.Send(
THttpRequest.Create(hmGet, 'https://api.example/things'));
// Response.StatusCode and Response.Headers are ready here.
// Read Response.Body until Eof to get the body.
end;A caller can change the protocol behavior:
uses Http2.Client, Http2.Tls; // Http2.Tls gives the TClearTextPolicy values
// Accept cleartext h2c as prior knowledge.
Client := THttpClientFactory.Create
.WithClearText(ctPriorKnowledge)
.Build;
// Speak plain HTTP/1.1 to a cleartext server, with no Upgrade request.
Client := THttpClientFactory.Create
.WithClearText(ctHttp1)
.Build;
// Accept an HTTP/1.1 peer when the peer does not offer h2.
Client := THttpClientFactory.Create
.WithHttp1Fallback(True)
.Build;TClearTextPolicy lives in Http2.Tls. You must name that unit in your
uses clause to write the value ctPriorKnowledge, ctUpgrade or ctHttp1.
A client reaches each origin directly or through an HTTP proxy, chosen per request:
// Read http_proxy, https_proxy and no_proxy when the client is built.
Client := THttpClientFactory.Create
.WithProxyEnvironmentVars
.WithNoProxy('.internal.example')
.Build;
// One proxy URL for every scheme (the manual form of all_proxy).
Client := THttpClientFactory.Create
.WithAllProxy('socks5://user:pw@127.0.0.1:1080')
.Build;
// A fixed proxy.
Client := THttpClientFactory.Create
.WithProxy('proxy.corp', 3128)
.Build;
// A SOCKS5 proxy; the proxy resolves host names.
Client := THttpClientFactory.Create
.WithProxyEnvironmentVars // e.g. https_proxy=socks5://user:pw@127.0.0.1:1080
.Build;
// A selector returns a proxy per URI, or nil for a direct connection.
Client := THttpClientFactory.Create
.WithProxySelector(MySelector) // MySelector implements IProxySelector
.Build;A selector overrides WithProxy, which overrides WithAllProxy, which overrides the environment. The
environment is read only when WithProxyEnvironmentVars is on. Proxies are
reached with a CONNECT tunnel, over TLS for an https:// proxy value, or a SOCKS5 CONNECT for socks5://; see doc/design/proxy.md.
For a test against a private certificate, use WithCACertFile.
For a self-signed certificate, use WithInsecureTls.
A server that asks for a client certificate is answered with an identity: a
certificate chain and its private key, loaded from PEM files, PEM in memory or
a PKCS#12 bundle. The client presents it on every TLS connection to an origin:
HTTP/2, the HTTP/1.1 fallback, SSE and wss, directly or inside an HTTP
CONNECT, SOCKS5 or https:// proxy tunnel. A proxy that requires a client
certificate of its own is answered with WithProxyClientIdentity, a separate
identity that goes to an https:// proxy only and never to the origin.
Client := THttpClientFactory.Create
.WithCACertFile('ca.pem')
.WithClientCertificateFiles('client-chain.pem', 'client.key', 'passphrase')
.Build;
// or load once, inspect, and share
Identity := TClientIdentity.FromPkcs12File('client.p12', 'passphrase');
WriteLn(Identity.Subject, ' ', Identity.Fingerprint, ' ', DateTimeToStr(Identity.NotAfter));
Client := THttpClientFactory.Create.WithClientIdentity(Identity).Build;A bad identity (unreadable file, wrong passphrase, a key that does not match
the certificate, an expired certificate) raises EHttpTlsConfigError from
Build; the message never contains the passphrase. A server that refuses the
certificate raises EHttpTlsClientAuthError with the TLS alert in Alert
(certificate required, unknown ca, certificate expired, ...), under TLS
1.2 and TLS 1.3 alike. It is never retried and never falls back to HTTP/1.1.
Cleartext (http, ws) connections ignore the identity. With an
IHttpLogger installed, a debug line names the identity's subject and
fingerprint, and nothing else about it. examples/mtls_get.pas is a short
program; see doc/design/mtls.md.
A different identity per origin, or a certificate that is replaced while the process runs, comes from a selector that is asked for each new TLS connection (not for a request that reuses one):
type
TByOrigin = class(TInterfacedObject, IClientIdentitySelector)
function Select(const AHost: string; const APort: Word): IClientIdentity;
end;
function TByOrigin.Select(const AHost: string; const APort: Word): IClientIdentity;
begin
if AHost = 'billing.example' then
// read fresh each time, so a renewed file is presented by the next connection
Result := TClientIdentity.FromPemFiles('billing.pem', 'billing.key')
else
Result := nil; // present no certificate
end;
Client := THttpClientFactory.Create
.WithClientIdentitySelector(TByOrigin.Create)
.WithProxyClientIdentity(ProxyIdentity) // for an https proxy that asks for one
.Build;The pool keeps connections that used different identities apart, but
MaxConnectionsPerHost counts an origin across identities: connections that
used a replaced certificate serve until they close, and an idle one is closed
to make room when the cap is reached. A busy connection is never closed for
that. An answer from the selector whose certificate has expired fails the dial
with EHttpTlsConfigError before anything is dialled. A client built with
WithSocketFactory and an identity, a selector or a proxy identity raises
EHttpTlsConfigError at Build unless the factory can present identities. The selector is asked
with the origin's host and port on every route, direct or through a tunnel.
The identity given with WithProxyClientIdentity goes to an https:// proxy
only, and the origin identity never goes to the proxy.
When the services are configured outside the client and only the caller knows which one needs which certificate, attach the identity to the request. It wins over the selector and the client's fixed identity, works through every route and goes to the origin only:
Resp := Client.Send(THttpRequest.Create(hmGet, 'https://billing.example/')
.WithClientIdentity(BillingIdentity)); // also IWebSocketRequest, SseRequestConnections that used different request identities never share a connection. A
redirect to the same origin keeps the request identity; a redirect to another
origin drops it. Send raises EHttpTlsConfigError before dialling when the
identity has expired, or when an injected socket factory cannot present it.
WithTextBody and ReadText live in Http2.Client:
uses Http2.Client;
Response := Client.Send(
WithTextBody(THttpRequest.Create(hmPost, Url), 'hello'));
WriteLn(ReadText(Response));JSON and XML live in the optional Http2.Readers unit. The read helpers
parse the whole body and return an object you own (free it):
uses fpjson, Http2.Client, Http2.Readers;
Sent := TJSONObject.Create;
Sent.Add('name', 'widget');
Response := Client.Send(WithJsonBody(THttpRequest.Create(hmPost, Url), Sent));
Got := ReadJsonObject(Response); // TJSONObject, caller-owned
try
WriteLn(Got.AsJSON);
finally
Got.Free;
end;The same shape works for XML: WithXmlBody / ReadXmlDocument.
ReadJsonData and the TJsonReader<T> class decode any TJSONData
descendant (arrays, numbers, strings). A body that does not parse raises
EHttpProtocolError, so a malformed response never leaks EJSONParser or
EXMLReadError. The XML reader also accepts documents that declare
encoding="us-ascii", which fcl-xml rejects on its own.
THttpClient.PoolStats returns a snapshot of the connection pool, for a
gauge callback to poll or for diagnosis:
Stats := (Client as THttpClient).PoolStats;
WriteLn(Stats.Connections, ' open, ', Stats.IdleConnections, ' idle, ',
Stats.ActiveStreams, ' streams, ', Stats.PendingAcquires, ' waiting');
for Origin in Stats.PerOrigin do
for Conn in Origin.Details do
WriteLn(Origin.Origin, ' age ', Conn.AgeMs, ' ms, idle ', Conn.IdleMs, ' ms');The snapshot is a copy: it holds no reference into the pool, and it is all
zeros once the client is closed. Take one snapshot per polling cycle and read
every gauge from it. See doc/design/request-observability.md.
WithRequestObserver installs an observer that sees every attempt (a request,
a redirect hop, a transparent retry), injects headers into it and is told when
it ends. Derive from TBaseRequestObserver and override what you need:
type
TTraceObserver = class(TBaseRequestObserver)
function OnAttemptStart(const A: THttpAttempt): IInterface; override;
procedure OnAttemptEnd(const AToken: IInterface;
const O: THttpOutcome); override;
end;
function TTraceObserver.OnAttemptStart(const A: THttpAttempt): IInterface;
begin
// a fresh value per attempt; the caller's request is not touched
A.Headers.SetValue('traceparent', NewTraceParent(A.Context));
Result := nil;
end;
Client := THttpClientFactory.Create
.WithRequestObserver(TTraceObserver.Create)
.Build;
Response := Client.Send(THttpRequest.Create(hmGet, Url).WithContext(ParentSpan));OnAttemptEnd fires once per attempt, when the body ends, fails or the
response is released (an unread response reports Abandoned; a deliberate
Close or Abort, and any failure, do not). A WebSocket session is one
attempt of kind akWebSocket: the handshake accepts injected headers, and the
outcome carries the message and byte counters, ClosedBy, CloseCode and
CloseReason. The outcome
carries the status, protocol, whether the connection was reused, byte counts
and timings. A callback runs outside every library lock, must not block, and
is swallowed and reported to WithLogger if it raises. A response that is
never read and never released keeps its attempt open. See
doc/design/request-observability.md.
A Server-Sent Events request (SseRequest) is an attempt of kind akSse, and
the connections of one TSseReconnectLoop share a StreamId and carry a
Reconnect count. Its outcome counts events rather than frames and times them
at the wire: MessagesIn, FirstMessageUs, MaxGapUs and the other
aggregates give time to first event, inter-event latency and stalls. An
observer that also implements IHttpMessageObserver and answers
[mkSseEvent] from WantsMessages receives each event with its type, id,
size, arrival time and the gap since the previous one; without that opt-in no
per-event work is done. See doc/design/server-sent-events.md.
The request observer is the seam for tracing, metrics and logging. The library
depends on none of them: it hands an adapter the attempt, the headers to
inject into and the outcome, and the adapter turns those into spans,
instruments and log lines. Three working adapters are in examples/, over
small in-memory stand-ins for the tracer and the metric registry, so a
consumer can copy one and swap the stand-in for OpenTelemetry, Prometheus or a
logging framework:
| Unit | What it does |
|---|---|
observer_tracing.pas |
reads the parent span from WithContext, injects a W3C traceparent per attempt (redirect hops and retries are sibling spans), sets status and error, ends the span |
observer_metrics.pas |
derives time to first byte, time to first event, mean inter-event latency, max gap, event and byte rates and total latency from the outcome; counts requests by status; keeps an in-flight gauge; reads PoolStats for pool gauges; and feeds an inter-event histogram per event when you switch the gate on |
observer_logging.pas |
writes one structured line from every callback, including redirect, retry, abandoned and error |
Install them side by side. The observer registered first wraps the others: start callbacks run in registration order and end callbacks in reverse.
Client := THttpClientFactory.Create
.WithRequestObserver(TTracingObserver.Create(Tracer))
.WithRequestObserver(TMetricsObserver.Create(Registry))
.WithRequestObserver(TLoggingObserver.Create(Logger))
.Build;
Registry.AddCollector(TPoolStatsCollector.Create(Client));
Response := Client.Send(THttpRequest.Create(hmGet, Url).WithContext(ParentSpan));examples/observer_all.pas runs a plain request, an SSE stream with a
reconnect and a WebSocket session through all three against the reference
servers in tools/validate, and prints the trace, the metrics and the log.
make measure-observer measures what the observers cost on loopback; the
method and the results are in doc/verification/observer-overhead.md. See
doc/design/request-observability.md, "Writing an adapter".
The examples/ directory has eleven small programs and three adapter units. Build
them all with:
make examples # or: task examplesThey compile into bin/ and are not run by the build. Run one with the
OpenSSL path set (OPENSSL_LIBPATH is required at runtime):
export OPENSSL_LIBPATH=/opt/homebrew/opt/openssl@3/lib
bin/basic_get https://nghttp2.org/
bin/post_text https://nghttp2.org/httpbin/post
bin/json_request https://nghttp2.org/httpbin/post
bin/xml_request https://nghttp2.org/httpbin/xml
bin/threaded_get https://nghttp2.org/ 8 25
bin/sse_stream http://127.0.0.1:8091/events # tools/validate/sse_server.py
https_proxy=http://proxy.corp:3128 bin/proxy_env https://nghttp2.org/
bin/websocket_echo ws://127.0.0.1:8601/echo hello # tools/validate/ws_servers.py
bin/observer_all http://127.0.0.1:8091 ws://127.0.0.1:8601/echo # both fixtures
make measure-observer # observer overhead on loopback| Program | Shows |
|---|---|
basic_get.pas |
The factory, one GET, status/headers, ReadText. |
post_text.pas |
WithTextBody streaming a plain text request body. |
json_request.pas |
WithJsonBody + ReadJsonObject (TJSONObject). |
xml_request.pas |
WithXmlBody + ReadXmlDocument (TXMLDocument). |
threaded_get.pas |
Many threads sharing one client and its pool. |
sse_stream.pas |
SseRequest + TSseReconnectLoop over an event stream. |
proxy_env.pas |
A GET routed by the proxy environment variables. |
mtls_get.pas |
GETs, or a wss session, with a client certificate, a per-origin selector, a per-request identity or an https proxy identity; reports a rejection. |
websocket_echo.pas |
Open a WebSocket, send a message, print the echo, close. |
observer_all.pas |
The tracing, metrics and logging adapters together on a request, an SSE stream with a reconnect and a WebSocket. |
observer_overhead.pas |
What the observers cost on a loopback request. |
observer_tracing.pas, observer_metrics.pas, observer_logging.pas |
The adapter units the two programs above use (not programs). |
| Path | Content |
|---|---|
src/ |
The library units. |
examples/ |
Eleven small, runnable programs and three observer adapters. See Examples. |
test/ |
The unit suite, the mock socket, and the h2probe CLI. |
tools/validate/ |
The interop, sanity, and harness scripts. |
bin/ |
The build output. |
third_party/ |
The pinned mORMot2 and harness checkouts. |
doc/ |
The design notes and the validation record. Read them first. |
LICENSE |
The Apache License, Version 2.0. |
NOTICE |
The copyright and the third-party components. |
type
TChat = class(TWebSocketListener)
public
procedure OnOpen(const ASocket: IWebSocket); override;
procedure OnText(const ASocket: IWebSocket; const AText: string;
const ALast: Boolean); override;
end;
procedure TChat.OnOpen(const ASocket: IWebSocket);
begin
ASocket.Request(1); // nothing is delivered until requested
end;
procedure TChat.OnText(const ASocket: IWebSocket; const AText: string;
const ALast: Boolean);
begin
WriteLn(AText);
ASocket.Request(1);
end;
var
Socket: IWebSocket;
begin
Socket := Client.Send(
TWebSocketRequest.Create('wss://example.com/chat', TChat.Create)
.WithSubprotocol('chat'));
Socket.SendText('hello');
Socket.Close(1000, 'bye');
end;The connection speaks HTTP/1.1 (RFC 6455); a WebSocket is not pooled. See
doc/design/websocket.md.
doc/http2client.mdgives the document map.doc/design/client-api.mdgives the full factory surface.doc/design/proxy.mdgives the proxy routing: environment variables, the no-proxy list, andIProxySelector.doc/design/mtls.mdgives mutual TLS: the identity loaders, where the identity is presented, the per-origin selector and certificate rotation, the proxy identity, and how a rejection is reported.doc/design/websocket.mdgives the WebSocket client: handshake, framing, delivery and demand, closing, and the reference implementations.doc/design/messages.mdgives the request and response types.doc/design/server-sent-events.mdgives the SSE parser, source, and reconnect/resume loop.doc/design/errors-redirects.mdgives the exceptions and the timeouts.doc/design/request-observability.mdgives the request observer, the timing and the adapters.doc/verification/validation.mdgives the validation results and the reasons.doc/verification/toolchain.mdgives the exact tool versions and revisions.
The validation record refers to the generated table at
bin/validation-results.md and to the raw sweep log at
doc/verification/harness-run-full.log.
-
The harness image is not on a registry. You must build it from the source.
-
Case A.8 of the interop gate is a SKIP. See the section above.
-
The
http2/http2-testsuite uses draft-09 and cleartext. No conformance claim depends on it. -
h2specis a server test tool. It does not apply to this client. -
The cleartext warning is not wired to the observer:
IHttp2Observerhas no cleartext event, so a cleartext request is silent. Seedoc/design/open-questions.mdanddoc/design/fallback.md. -
Server-Sent Events are full-featured over HTTP/2. Over the HTTP/1.1 fallback they are best-effort: an idle stream is bounded by the socket read timeout and a blocked read cannot be cancelled. See
doc/design/server-sent-events.md. -
WebSockets are RFC 6455 over HTTP/1.1 only: no RFC 8441, no
permessage-deflate, no automatic reconnection. Seedoc/design/websocket.md. -
The observer seam has no event for a whole SSE reconnect loop: each connection is an attempt, linked by
StreamIdandReconnect, and a span for the whole stream is the caller's. The outcome does not say which kind of attempt ended, so an adapter keeps the kind in its token. A response, SSE source or WebSocket that is never read to its end and never released holds its attempt open. -
Arrival times are taken where the library first sees the data. Events packed into one frame share a stamp, so a per-event gap histogram is quantised to frames, and a consumer that stops reading for a full inbound queue delays the stamps of every stream on that connection. A WebSocket reads on demand, so its stamps include the consumer's wait unless
WithAutoRequestis on. Seedoc/design/request-observability.md, "Limits". -
The observer overhead has no committed bound yet; the measurement is
make measure-observerand the record isdoc/verification/observer-overhead.md. -
Proxies are reached by
CONNECTtunnel only. Anhttp_proxyfor anhttp://origin tunnels instead of forwarding, and many proxies refuseCONNECTto ports other than 443; asocks5://proxy has no such restriction. SOCKS4 and SOCKS5BIND/UDP ASSOCIATEare not implemented. Seedoc/design/proxy.md.
This product is licensed under the Apache License, Version 2.0.
See the LICENSE file for the full text.
Copyright 2026 Liam Seamus Coughlin.
The library uses mORMot2 for TLS and ALPN. The proxy-routing code is
self-contained but follows mORMot2's designs for URL parsing (TUri), CIDR
matching (TIp4SubNet), loopback detection and the CONNECT exchange; see
doc/design/proxy.md, section "mORMot2 as a reference". mORMot2 keeps its own
license.
See the NOTICE file for the full list of third-party components.