From cfca251957a6a97eded0b6a3d972fac46e55eb1d Mon Sep 17 00:00:00 2001 From: freitasjca Date: Sat, 22 Aug 2026 18:40:57 +0100 Subject: [PATCH] fix(streaming): exclude HORSE_PROVIDER_NGHTTP2 from the default stream-writer registration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/Horse.Response.pas | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Horse.Response.pas b/src/Horse.Response.pas index cc353d4..ac9a69b 100644 --- a/src/Horse.Response.pas +++ b/src/Horse.Response.pas @@ -1359,9 +1359,25 @@ function DefaultWebBrokerStreamWriterFactory(const AResponse: THorseResponse): I end; initialization +{ FIX-STREAM-FACTORY — every provider that supplies its own stream writer must + be excluded here, because FStreamWriterFactory is a last-writer-wins class var + and BOTH registrations run from unit initialization sections. Whichever + initializes second silently wins, and that order is decided by the compiler's + dependency walk — not by anything in this source. + + HORSE_PROVIDER_NGHTTP2 was missing from this list. On FPC trunk the provider's + own factory happened to initialize last and streaming worked; on FPC 3.2.2 the + order differs, this WebBroker default won, and the nghttp2 transport answered + every streaming request with total silence — no headers, no body, client + timeout. Nothing else was affected, which is what made it hard to find. + + Note that the three providers already listed were excluded for exactly this + reason. Adding the fourth restores the intent; it does not change behaviour on + any build where the order already happened to favour the provider. } {$IF NOT DEFINED(HORSE_PROVIDER_IOCP) AND NOT DEFINED(HORSE_PROVIDER_HTTPSYS) AND - NOT DEFINED(HORSE_PROVIDER_EPOLL)} + NOT DEFINED(HORSE_PROVIDER_EPOLL) AND + NOT DEFINED(HORSE_PROVIDER_NGHTTP2)} THorseResponse.RegisterStreamWriterFactory(DefaultWebBrokerStreamWriterFactory); {$ENDIF}