Releases: freitasjca/Delphi-nghttp2
Release list
1.21.0 — enum fields that were never on the wire
Enum fields were silently dropped on the wire (ENUMRTTI-1)
A proto enum with non-contiguous values — 0,1,2,13,14 or 0,100,150,200, which
real schemas use constantly — produced a Pascal enum with no RTTI. The compiler
dropped the published property, so the codec never saw the field: neither encoded
nor decoded, and nothing raised.
Confirmed on both compilers. Neither Delphi 12 nor FPC trunk refuses the
declaration; both compile it and omit the property from RTTI. If you have a sparse
enum, you were losing that field on the wire regardless of toolchain.
Scale, and the layer that number describes: on the 7,301-schema googleapis corpus
this affected 1,405 schemas / 5,633 properties. All 1,405 still generated code
that compiles — compiling was never what was broken, which is why no gate caught it.
Generated output moves
Two ways, and only where it must:
- Enum members are emitted ordered by value. Each keeps its own number; only
declaration order changes. Nothing on the wire changes. - A field whose enum is still non-contiguous after ordering is published as
System.Int32(TArray<System.Int32>when repeated). The enum type is still
declared and still usable — it is the published property that changes type.
Identical varint on the wire.
If your enums are contiguous, your generated code is unchanged and your working
build is safe.
Assigning an enum constant to an affected field now needs Ord(). Those are exactly
the fields that were never transmitted, so no working code depends on the old spelling.
Also in this release
TNghttp2Client.ConnectTimeoutMS— a bounded TCP connect. Without it an
unreachable peer could stall ~127 s on Linux with no way to interrupt.protoc-oracle.shdeclaredsint32/fixed32unsupported for three releases
after WIRE-FORM-1 made them selectable; the differential gate had been failing on
a stale expectation.ProtoOptionalProbenow pins non-contiguous enum behaviour on both compilers,
so this cannot regress unnoticed.- Docs: architecture diagram,
CONTRIBUTING.md,CHANGELOG.md, and a wiki at
https://deepwiki.com/freitasjca/Delphi-nghttp2
1.20.0
What's new in 1.20.0
IPv6 listener + poll() in SocketWaitImpl
CreateListenerSocket6 / AcceptConnection6 start a second accept thread bound to :: with IPV6_V6ONLY=1, so the existing IPv4 listener is not disturbed. On IPv4-only hosts the call returns INVALID_SOCKET_HANDLE and an error string rather than raising.
SocketWaitImpl replaces select with fpPoll / poll on FPC/UNIX and Delphi POSIX, removing the silent no-timeout fallback for file descriptors ≥ 1024.
Bounded RSS on large streaming downloads — CL3b
PushStreamData was calling Seek(0, soBeginning) after every append, destroying the read cursor that ReadResponseBodyCallback uses. The buffer grew without bound and the worker deadlocked permanently under slow-client conditions.
Fixed by saving FStreamBuf.Position before seeking to end and restoring it after the write. Gate: tests/Nghttp2FloodRead.dpr downloads 64 MB in 1 024 × 64 KB ReadChunk calls with < 8 MB peak client RSS.
Connection reuse — Reconnect, Ping, GOAWAY (CL4)
Reconnectopens a fresh session to the same host/port without reallocating the client objectPing(ATimeoutMS)submits an HTTP/2 PING frame and pumps until ACK or timeoutGoAwayReceivedproperty — set when the server sends GOAWAY- A stream closed with
REFUSED_STREAM(error 7) now gets a targeted error message pointing toReconnect
Group-B scalar wire forms — WIRE-FORM-1
TProtoMemberWireForm (pwfDefault, pwfZigZag, pwfFixed) selects the wire encoding for sint32/sint64 and the fixed/sfixed family via a second TProtoMemberAttribute constructor overload. protogen emits [TProtoMember(N, pwfZigZag)] for sint* and [TProtoMember(N, pwfFixed)] for fixed*/sfixed* automatically. The 7 googleapis schemas previously refused for Group-B scalars now compile.
Upgrade notes
The doc/ files update all corpus figures and refusal lists. Run compile-check.sh --all after upgrading to get the post-WIRE-FORM-1 acceptance count.
1.19.0
Added
B5 — a no-overlap ALPN refusal now explains itself, identically on every
OpenSSL version.
A peer sharing no ALPN protocol with this client sends a fatal
no_application_protocol alert (alert 120, RFC 7301 §3.2). The handshake fails,
and the client used to report whatever prose the local OpenSSL had for reason
1120:
OpenSSL 3.0.13 ERR=error:0A000460:SSL routines::reason(1120)
OpenSSL 3.6.0 ERR=error:0A000460:SSL routines::tlsv1 alert no application protocol
Same error code, different text. Neither names ALPN, neither names h2, neither
names the endpoint — and the two disagree, so a message seen on one machine is
unsearchable from another.
The refusal is now recognised by its reason code, never its text, and
reported as:
ALPN: 127.0.0.1:19313 refused the handshake - it shares no ALPN protocol with
this client, which offers "h2" only and has no HTTP/1.1 fallback. The peer sent
a fatal no_application_protocol alert (alert 120, RFC 7301 3.2), so the
connection never completed. Use an HTTP/2 server, or h2c (no TlsContext) if the
endpoint is cleartext HTTP/2.
The raw OpenSSL string is retained as evidence rather than discarded.
New public API — all additive
NghttpsslLastErrorEx(out ACode): string— text and code from one pop.
ERR_get_errorconsumes the entry, so two calls cannot get both.NghttpsslReasonOf(ACode): Cardinal— the version-stable reason field.SSL_R_TLSV1_ALERT_NO_APPLICATION_PROTOCOL = 1120ENghttp2TlsAlpnRefused = class(ENghttp2Tls)— a subclass, so existing
on E: ENghttp2Tlshandlers keep working unchanged.
Upgrading
Nothing that compiled before stops compiling. NghttpsslLastError is unchanged
for its existing callers. The only break is code that matched the text of
this particular failure — and that text was never stable across OpenSSL
versions, which is precisely the defect fixed here.
Verification
Nghttp2AlpnMismatch's nooverlap arm went from 1 assertion to 6, now
demanding the same diagnostic quality as the noalpn arm: ALPN, host, port,
alert 120, and the no-shared-protocol explanation.
The cross-version pair is the evidence: FPC 3.3.1 against OpenSSL 3.0.13 and
Delphi 12 against 3.6.0 render different raw text and produce the identical
diagnosis. noalpn stays at 7 passed, unchanged.
1.18.2
Fixed
FIX-LOADRACE-1 — the FFI library loaders were not thread-safe.
NghttpLoad and NghttpsslLoad were if GLoaded then Exit(True) followed, an
entire library load later, by GLoaded := True — a check-then-set with no lock
between the two. Two threads arriving together both saw False and both ran the
whole sequence: the library opened twice, ~50 function pointers rewritten twice,
and two handles acquired where the unload path only ever releases one.
The OpenSSL loader was worse than no guard at all. Its Boolean re-entry
flag turned a second thread arriving mid-load into Exit(False), surfaced to the
caller as "OpenSSL could not be loaded" — while the load actually succeeded
moments later on the other thread. A spurious and thoroughly misleading failure,
on the one path whose job is to explain why TLS is unavailable.
Both loaders now use a TCriticalSection, as every other unit in src/ that
shares state across threads already did. Both Unload paths take the same lock;
they clear the loaded flag and release handles that a thread inside a load is
actively relying on.
Who was affected
Anyone constructing TNghttp2Client, TNghttp2Server, TTlsClientContext or
TTlsServerContext from more than one thread at startup — those four
constructors are what reach the loaders. A process that creates its first client,
server or TLS context on a single thread was never exposed, because by the time
any other thread arrives the load has completed and the fast return is correct.
Upgrading
No API change. No behaviour change for single-threaded initialisation.
TTlsClientContext.EnableHttp2Alpn and the loader entry points keep their
signatures and their semantics.
Honest limits of the verification
Neither test suite races first-load across threads, so neither can demonstrate
this fix. What they establish is that both units still compile on both
toolchains and that nothing regressed: FPC 3.3.1 at 37 passed / 0 failed / 1
skipped, Delphi 12 at ALL STAGES PASSED.
A gate that could observe it is cheap and is the intended follow-up: N threads
calling NghttpsslLoad simultaneously in a fresh process, asserting every one
returns True — because before this change, the losers of that race got a
spurious False.
1.18.1
Fixed
FIX-ALPN-RACE-1 — heap corruption when a TTlsClientContext is shared across threads.
1.18.0's TNghttp2Client.Connect called TTlsClientContext.EnableHttp2Alpn on
every connect, so that offering h2 did not depend on the order in which a
caller assigns and configures TlsContext. That goal was right; the placement
was not.
EnableHttp2Alpn is SSL_CTX_set_alpn_protos — it writes the context, and
a TTlsClientContext is designed to be shared. OpenSSL frees the stored
protocol list and allocates a replacement, unlocked, so two threads connecting
on one shared context free the same block twice. Measured at 4 aborts in 30
runs of the provider mTLS suite, reported by glibc as double free or corruption (fasttop) or malloc_consolidate(): invalid chunk size.
The abort surfaces wherever the allocator next notices the damage — typically a
sequential test well after the concurrent one that caused it — so the crash
location is not the fault location.
Fix: bind SSL_set_alpn_protos (per-connection, OpenSSL 1.0.2+) and apply
the list to the SSL in TTlsClientConnection.Create. Connect no longer
touches the context. Everything 1.18.0 wanted is preserved — applied on every
connect, independent of assignment order, always exactly h2 — and it now
mutates nothing shared.
After: 0 aborts in 30 runs, provider suite 37 passed / 0 failed / 1 skipped.
Upgrading
No API change. TTlsClientContext.EnableHttp2Alpn remains public and
idempotent; it is simply no longer required. If you call it yourself, call it
once, before any thread connects — a shared context must not be
reconfigured while other threads are connecting on it.
Anyone on 1.18.0 using a shared TTlsClientContext from more than one thread
should upgrade.
1.18.0 — a client that resolves, negotiates, times out and streams
chore(release): 1.18.0
A client release. Minor per doc/releasing.md section 2: everything here is
additive - new methods and a new optional parameter - with nothing removed and
no wire-format change.
49fdac5 CL1 ConnectToHost resolves host names and IPv6 on all four
platform arms. It took an IPv4 literal only, and two of the
three branches never checked the parse, so a host NAME became
a connect to 0.0.0.0 or 255.255.255.255 rather than an error.
fc02cb4 CL2a :scheme follows the transport. Every request over TLS
advertised :scheme: http (RFC 7540 8.1.2.3).
1779040 CL2b ALPN is offered by Connect itself, and refusal names what
happened instead of rendering it as 'server selected ""'.
CL2c The request timeout expires. It was documented and checked,
but only BETWEEN reads: DoRead went straight to a blocking
recv, so a peer that accepted and then said nothing parked the
client permanently. Real in the source, inert at runtime.
CL3a Incremental response delivery. BeginRequest(...,
AStreamResponse := True) opts one stream out of whole-body
buffering; ReadChunk drains it as it arrives.
New API, all additive:
TNghttp2Client.ReadChunk(AStreamId; var ABuffer; ACount, ATimeoutMS)
> 0 bytes copied = 0 end of stream < 0 timed out, still open
TNghttp2Client.BeginRequest(..., AStreamResponse: Boolean = False)
TTlsClientConnection.ReadTimeoutMS (0 = block forever, the old behaviour)
Existing callers are unaffected: the new parameter defaults to False and
ReadTimeoutMS defaults to 0, so a stream that does not ask for incremental
delivery or a deadline behaves exactly as before.
Two behaviour changes worth knowing about, neither of which alters the wire
format:
- A TLS connection whose peer does not negotiate h2 is now REFUSED at Connect
with a specific message. It was already refused; the message was misleading. - TakeResponse RAISES on a streaming stream that still has unread bytes,
rather than returning an empty Body that looks like a server which sent
nothing.
Known limits, documented in the code rather than left to be discovered:
- Connect still has no timeout of its own. ConnectToHost is a blocking
connect(), so a blackholed SYN hangs one layer earlier than this fixes. - SocketWaitImpl reports any fd >= 1024 as readable WITHOUT waiting, because
select's fd_set cannot represent it, so the read timeout degrades under
heavy descriptor pressure. - ReadChunk delivers incrementally, but arrival-before-END_STREAM is only
observable under async dispatch, where the host owns the worker pool.
Gates, both toolchains, FPC trunk 3.3.1 and Delphi 12 dcc64:
Nghttp2AlpnMismatch 7/0 and 3/0 first execution of the ALPN error path
Nghttp2ReadTimeout 6/0 1503-1515 ms against a 1500 ms budget
Nghttp2StreamRead 14/0/1 the skip is structural and stated
Whole suite green around them on both compilers. The generator did not change,
so the corpus sweeps are carried forward from 1.17.0 rather than re-run:
compile-check.sh --all 7,230/7,301 with 0 emitter defects, corpus-check.sh
7,287 accept / 14 refuse / 0 unknown / 0 error.
The provider floor stays at >=1.10.0 per section 6 - horse-provider-nghttp2
does not call the new API yet, and raising a floor gratuitously forces
downstream upgrades for no benefit.
1.17.0 — zero emitter defects
Delphi-nghttp2 1.17.0
Four generator fixes, all of the same family: a proto field or enum value whose
Pascal spelling collides with something the generated code itself relies on.
Every one of them changes generated identifiers, which is what makes this a
minor rather than a patch — downstream sees different code even though nothing
breaks.
The shadowing family, now closed on all three declaration kinds
A generated class is not a closed world: its own property names sit in scope
inside its own method bodies, its enum values sit at unit scope, and an
inherited method can be hidden by a published property. Four defects, four
different blast radii:
| what shadows what | reach | |
|---|---|---|
FIX-SHADOW-2 (11a8cd5) |
a field named high shadows the intrinsic |
inside its own class's methods |
SHADOW-2b (0c05091) |
a field named free hides inherited TObject.Free |
other classes holding one as a field |
SHADOW-3 (0c05091) |
an enum value SYSTEM shadows the System unit |
every qualified RTL call in the unit |
| UNIT-SHADOW-1 (new) | a field named system shadows the System unit |
every System.Length/High/SetLength in its class |
SHADOW-3 and UNIT-SHADOW-1 are the same collision reached from the two
directions a Pascal identifier can arrive from. 11a8cd5 qualified emitted
intrinsics as System.High / System.Length / System.SetLength on the
grounds that a qualified reference cannot be shadowed — and then both halves of
this release were the discovery that it can be, once by an enum value and once
by a property.
google.cloud.dataplex.v1 declares string system = 11 (EntryType) and
string system = 2 (EntrySource); both classes also carry map fields, so both
emitted System.Length(Flabels) and got Error: Illegal qualifier. Those
three schemas — approval_workflow, catalog, data_products — were the only
DID NOT COMPILE cases left in the corpus.
system now renames to sys, following the existing string -> str
abbreviation. Unconditionally, and that distinction matters: the FIELDWORD-1
cases (length, high) only collide when a class happens to emit that
intrinsic, but System. is emitted for every repeated and map accessor, so
there is no per-message narrowing to be had.
FIX-IDENT-1 now bounds generated TYPE names (6429a64)
The identifier-length guard covered enum values only; long generated type
names could still be emitted unbounded. Now both.
Corpus
compile-check.sh --all, all 7,301 googleapis schemas:
| COMPILED | emitter defects | FPC crashes | refused | |
|---|---|---|---|---|
| 1.16.0 | 7,216 | 7 | 57 | 21 |
| 1.17.0 | 7,230 | 0 | 50 | 21 |
No schema in the corpus is now rejected because of Pascal we emitted. The
remaining 71 are 21 deliberate refusals (Group B scalars, Api/DescriptorProto,
and the bigquery enum declaring both minimal and MINIMAL) and 50 on which
FPC itself falls over.
The shape of the result is worth as much as the number: UNIT-SHADOW-1 moved
the defect column by exactly 3 and left crashes and refusals untouched at
50/21. A fix that also moved those would have needed explaining.
corpus-check.sh (parse + emit) — run for this release, not carried
forward: ACCEPT 7,287 · REFUSE known 14 · REFUSE UNKNOWN 0 · ERROR 0,
unchanged from 1.16.0. That it did not move is the useful part: all four
fixes rename emitted identifiers, so none of them should change whether a
schema is accepted, and the flat result is what confirms nothing regressed
underneath the compile improvement.
Its refusal bucket (14) and compile-check.sh's (21) count different things
and do not reconcile by arithmetic. Quote each with the tool that produced it.
The compiler crashed column counts FPC falling over, not our output being
rejected. Internal error 2015071503 remains unattributed (noted against
oracledatabase in 0c05091, and it also hits analytics/admin); an earlier
theory this session that it was the >=127-character identifier limit was
measured and disproved — the longest identifier in a crashing unit is 75
characters, and a same-file type reference never takes the unit prefix that
would have made it longer.
Upgrading
Regenerate. A proto field named system or free, or an enum value SYSTEM,
now emits under a different Pascal spelling, so hand-written code referring to
the old name needs the new one. Nothing else moves.
1.16.0 — protogen follows imports
protogen follows import (IMPORT-1)
protogen previously generated from one .proto and refused anything that
imported another. It now loads the whole import closure and emits one unit per
file, with path-derived unit names and qualified cross-file references.
Generated output moves. Two name-resolution fixes ride along, and both can
rename identifiers in code that compiled before:
- FLATTEN-1 — nested types are flattened into unit scope, so two nested
types sharing a leaf name used to collide. They are now disambiguated. - SCOPE-1 — type names resolve innermost-outward from the enclosing scope,
as proto requires. Names that previously resolved to the wrong type, or
failed to resolve, now resolve correctly.
Where your schemas already generated compiling code, output is unchanged. The
renames land where the old output did not compile or bound the wrong type.
Corpus
7,216 of 7,301 schemas now generate code that compiles — parses, emits,
and survives the compiler — up from 7,204, with emitter defects down 22 → 7.
The denominator changed too, and that is the more useful number: the previous
3019/3019 counted only schemas the generator did not refuse, so it excluded
59% of the corpus and improved every time the generator refused more. FPC
compiler crashes (57) are now counted apart from emitter defects, having once
overstated the defect count by 3.5x.
Tests — the transport is now actually exercised
grep -rl TNghttp2Server tests/ returned nothing before this release. Every
stage was codec or codegen, so a fully green run had never opened a socket —
not hypothetical: this suite passed for years on a machine with no
nghttp2.dll on it.
Nghttp2ServerSmoke drives the library's own client against its own server in
one process (bind, h2c, GET /smoke → 200 + body, unrouted → 404, clean stop).
Wired in as stage 4b on Windows and 3c on FPC. Absent libnghttp2 it exits 3 and
skips loudly — a quiet skip is indistinguishable from a pass, which is what
let the gap survive.
This is a floor, not depth: TLS, mTLS, the epoll loop, streaming and graceful
shutdown remain covered by horse-provider-nghttp2's suite, Linux/FPC only.
Also
samples/rest-and-grpc— REST through Horse over HTTP/1.1 on:9000and
gRPC through this library on:50051, one binary. Horse providers are
mutually exclusive; serving gRPC needs no provider at all.- Runtime exception messages are ASCII — em-dashes and typographic quotes made
them unreadable in a Windows console.
Requires libnghttp2 >= 1.59 at run time. The gRPC layer needs FPC trunk; the
transport builds on 3.2.2.
1.15.0 — generated Pascal compiles
Seven emitter defects, all found by a new tool that generates from real schemas and hands the result to a compiler. That compiler had previously seen four hand-picked fixtures; it has now seen 3,019 googleapis schemas, and all 3,019 build.
The largest: a bytes field produced a unit that would not compile, because SysUtils was never named in the generated uses clause. Broken since the generator shipped — none of the four fixtures declares a bytes field.
Also fixed: enum values that are Pascal reserved words, that shadow intrinsics the generated code calls (HIGH broke the destructor's own High()), that shadow type names (DOUBLE broke TArray<Double>), or that exceed FPC's 126-character identifier limit; a bundled well-known enum inside a oneof losing its has-bit; and a oneof member named none colliding with the generated sentinel.
tools/protogen/compile-check.sh ships so this stays checkable.
Upgrading: safe from any 1.1x. Generated enum values change only where they previously produced code that did not compile.
1.14.0 — 99.5% of real proto3
Three more constructs now generate: message members inside a oneof (the common shape of a oneof — 19% of the googleapis corpus alone), optional on a message field (a no-op label in proto3), and enums sharing a value name (prefixed rather than refused).
Two of the three were refusals whose reasoning had expired: written before generated destructors existed, and never revisited.
Measured end to end for the first time: protogen parses and emits 99.5% of 7,301 real googleapis schemas. Earlier figures measured only parsing.