fix(streaming): exclude HORSE_PROVIDER_NGHTTP2 from the default stream-writer registration - #552
Conversation
…m-writer registration FStreamWriterFactory is a last-writer-wins class var, and both Horse core and a provider that supplies its own writer register into it from unit initialization sections. Which one survives is therefore decided by the compiler's dependency walk, not by anything in either source file. Three providers are already excluded from the default registration for exactly that reason. HORSE_PROVIDER_NGHTTP2 was missing. The consequence is not cosmetic. On FPC trunk 3.3.1 the provider's factory happens to initialize last and streaming works; on FPC 3.2.2 the order differs, the WebBroker default wins, and it cannot write to an HTTP/2 stream — so every Res.SendStream request returns complete silence: no headers, no body, no error, just a client timeout. Nothing else is affected, which is what made it hard to find. That also means streaming on trunk has been passing by accident of initialization order rather than by design, and could have flipped at any time. Verified: FPC 3.2.2 goes from curl exit=28 with zero bytes to five NDJSON records and exit=0; the full suite passes 24 stages with one explicit skip. FPC trunk 3.3.1 stays at 27/27, confirming this is a no-op where the ordering already favoured the provider.
|
Some additional evidence from re-validating this on a larger suite yesterday (2026-08-23), which I think strengthens the case for taking it. The failure does not present as a streaming bug. Without the clause, FPC 3.2.2 fails 9 stages, and only 2 of them point anywhere near the stream writer:
The first six look like transport or protocol defects — a TLS suite timing out, the event-loop driver hanging — and the run reports them that way. Checks 1–32 pass in 21 ms and then nothing. I spent a substantial part of a session diagnosing it as a protocol issue before finding the missing guard, which is what the ordering dependency really costs: not just silence on the wire, but silence that misattributes itself. Independent confirmation that it is server-side: stage 15 is driven by Updated numbers — the suite has grown since I opened this:
This also reinforces the follow-up suggestion at the end of the description. A guard list that must be updated by every provider author is a latent bug per provider; making |
regyssilveira
left a comment
There was a problem hiding this comment.
A correção é pequena, coerente com os guards já existentes para providers com stream writer próprio e elimina a dependência da ordem de inicialização no FPC 3.2.2. Aprovado para integração como primeiro passo da sequência.
Summary
Horse.Response.pasregisters its default WebBroker stream writer from a unitinitialization section, guarded to exclude providers that supply their own.
HORSE_PROVIDER_NGHTTP2is missing from that guard.Because
FStreamWriterFactoryis a last-writer-wins class var, and both Horsecore and the provider register into it from initialization sections, the
compiler's dependency walk decides which one survives — nothing in either
source file does.
src/Horse.Response.pas:1361(atmaster,ff41415):Three providers are already excluded for exactly this reason. Adding the fourth
restores the evident intent.
Why it matters more than a missing define
On FPC trunk 3.3.1 the provider's factory happens to initialize last, so it
wins and streaming works. On FPC 3.2.2 the order differs, the WebBroker
default wins, and it cannot write to an HTTP/2 stream — so every streaming
request returns complete silence: no headers, no body, no error. The client
simply waits until it times out.
That means streaming on trunk has been passing by accident of initialization
order, not by design. A compiler upgrade, a new unit, or a reordered
usesclause could flip it at any time, in either direction, silently.
Nothing else is affected — which is what made it hard to find. Ordinary
requests, TLS, mTLS, graceful shutdown and WebSocket all work normally; only
Res.SendStreamis silently redirected to a writer that cannot serve it.Reproduction
Build an nghttp2-provider server on FPC 3.2.2 with a
Res.SendStreamroute:exit=28is curl's timeout, with zero bytes received. Verbose output showsthe request sent and no response line at all — not even
< HTTP/2 200, whichdistinguishes this from a stalled body.
With the one-line fix:
Full suite on FPC 3.2.2 after the fix: 24 stages pass, 0 fail, 1 explicit skip
(gRPC, which needs trunk for
TCustomAttribute). On FPC trunk 3.3.1: 27/27unchanged, confirming the fix is a no-op where the ordering already favoured the
provider.
The change
One condition, plus a comment recording why the list exists — the failure mode
is invisible enough that the next person adding a provider will want to know.
No behavioural change on any build where the ordering already favoured the
provider, and none at all on Delphi or on builds that use the WebBroker writer.
A suggestion, not part of this PR
This guard scales by requiring every new provider to remember to add itself,
and forgetting produces a silent failure rather than an error. Two sturdier
shapes, if you would like either as a follow-up:
RegisterStreamWriterFactoryrefuse to overwrite an already-registeredfactory, so the first specific registration wins and core's default only
applies when nothing else claimed it.
initialization, removing the ordering dependency entirely.Both are larger changes than this one, and this PR fixes the immediate bug
without prejudging either.
Context
Found while making
horse-provider-nghttp2build on the FPC that Horse's own CIinstalls (
apt-get install -y fpc→ 3.2.2). Companion to #549, #550 and #551.