Skip to content

Prepare for next release #36

Merged
aborovsky merged 43 commits into
masterfrom
develop
Jul 3, 2026
Merged

Prepare for next release #36
aborovsky merged 43 commits into
masterfrom
develop

Conversation

@aborovsky

Copy link
Copy Markdown
Collaborator

No description provided.

JCMais and others added 30 commits May 22, 2026 10:51
The readable/end/error event handlers called unpause() synchronously,
which could invoke curl_easy_pause() while libcurl was still processing
the READFUNC_PAUSE return from the read callback. This reentrant call
caused hangs on Linux.

Defer the unpause to the next event loop tick via setImmediate, matching
the pattern used by setUploadStream in Curl.ts.
setImmediate fires in the check phase of the libuv event loop, but
libcurl's multi handle timeout fires in the timer phase. On Node.js 24
the setImmediate-based unpause wasn't being picked up reliably.

setTimeout(fn, 0) fires in the timer phase, matching libcurl's callback
scheduling and ensuring consistent behavior across Node.js versions.
The mime StaticReadCallback was returning CURL_READFUNC_PAUSE without
updating pauseState, so isPausedSend/isPausedRecv both stayed false.
This caused the test's conditional unpause (which checks pause state
before calling curl_easy_pause) to be a silent no-op, leaving the
transfer paused forever once libcurl needed a second read callback.

The test only passed when the stream's 'end' event fired before
libcurl's second read attempt — a race that resolved differently
across Node.js versions and platforms.

Additionally, the docs and examples incorrectly referenced
CurlPause.Recv. The mime read callback supplies upload data, so it
pauses SEND (matching Easy::ReadFunction). Updated docs, examples,
and tests to use Send.

Also reverted setTimeout back to setImmediate now that the underlying
race is properly fixed.
node-gyp v12.1.0+ adds support for Visual Studio 2026 (v145 build
tools), which the windows-2025 GitHub Actions runner has started
shipping ahead of the official June 2026 migration. The previous
pinned 11.4.2 couldn't detect the new VS install layout and failed
with "unknown version 'undefined' found at C:\Program Files\Microsoft
Visual Studio\18\Enterprise".

node-gyp v12 only breaking change is bumping the Node.js engine range
to ^20.17.0 || >=22.9.0, which this project already exceeds.

node-pre-gyp v2.0.3 brings the Node 24 ABI crosswalk entry plus minor
bug fixes (private S3 ACL support, revert of incompatible nopt v9).
Node.js 26 was released on 2026-05-05 and transitions to Active LTS on
2026-10-28. Add it to the build-and-release and build-lint-test
workflows so prebuilt binaries are published alongside the existing
22/24/25 set.
The Windows build script was forcing node-gyp to use Visual Studio
2022, but the windows-2025 runner is being migrated to Visual Studio
2026 ahead of the official 2026-06-15 cutover. Some image revisions
ship only VS 2026, causing node-gyp to fail with "Could not find any
Visual Studio installation to use" because the pinned 2022 wasn't
present.

Letting node-gyp auto-detect works because:
- node-gyp 12.1.0+ supports VS 2026 (already bumped to 12.3.0)
- VS 2022 (v143) and VS 2026 (v145) are binary-compatible at the
  C++ ABI level and share the same v14.x VC++ Redistributable, so
  whichever toolset the runner ships builds an equally usable
  prebuilt binary for end users
The Node 26 Docker image isn't published for Alpine 3.21 — only for
3.22 and 3.23 — so the matrix jobs failed with "manifest for
node:26-alpine3.21 not found". All four Node versions in the matrix
(22, 24, 25, 26) have an alpine3.22 variant, and musl is 1.2.5 across
3.20–3.23 so the prebuilt binary stays runtime-compatible for users
still on 3.21.
ngtcp2 1.17.0 builds the new libngtcp2_crypto_ossl backend (the legacy
quictls backend is gone now that OpenSSL 3.5+ has native QUIC). The
.pc file for that backend declares Requires libcrypto, so pkg-config
needs to resolve OpenSSL too when libcurl probes for ngtcp2.

On Ubuntu/macOS the system OpenSSL .pc lives in pkg-config's default
search path so this happens implicitly. The Alpine container image
has no system OpenSSL, the libngtcp2_crypto_ossl probe returns "no",
and libcurl falls back to assuming the (no-longer-existent) quictls
backend, then fails to compile vquic/curl_ngtcp2.c with:

    fatal error: ngtcp2/ngtcp2_crypto_quictls.h: No such file or directory

Add our statically-built OpenSSL's pkgconfig dir to PKG_CONFIG_PATH
when wiring up ngtcp2 so the libngtcp2_crypto_ossl probe succeeds on
every platform.
Move the [Unreleased] entries accumulated since 5.0.2 into a dated
[5.0.3] section and re-create an empty [Unreleased] block. Also
captured two CI fixes that landed during this cycle but didn't have
their own changelog lines (Alpine ngtcp2/ossl pkgconfig, Windows
VS 2026 detection) and a Changed entry for the node-gyp / Alpine
container bumps.

Version in package.json is intentionally not bumped — the release
workflow handles that.
The release prep flow grew enough decision points (when to reset stale
local master, semver patch-vs-minor judgment, capturing CI/dep changes
that didn't get their own [Unreleased] entries) that a short procedural
checklist wasn't pulling its weight. Convert to a skill so the guidance
loads automatically whenever release prep comes up, and delete the old
slash command — the skill subsumes it.
The accumulated changes since v5.0.2 include adding Node.js 26 prebuilt
binary support and an observable behavior change in CurlMimePart
(`isPausedSend` now correctly reflects state after mime read callback
returns CURL_READFUNC_PAUSE). Both qualify as minor under semver — Node
26 users couldn't use v5.0.2 at all, and code written against the old
buggy `isPausedSend` value will see different results.
When libcurl 8.17 introduced CURLMOPT_NOTIFYFUNCTION, our Multi handle
opted in by default. The notify callback fires from inside
curl_multi_socket_action, our handler synchronously resolves the
perform() promise from there, and microtask draining can then run the
.then() handler — including the curl_multi_remove_handle call — while
libcurl is still on its own call stack. libcurl rejects that with
CURLM_RECURSIVE_API_CALL (code 8): "API function called from within
callback".

Wrap the removeHandle + completion callback in setImmediate so libcurl
unwinds curl_multi_socket_action first. Guard the call with isOpen +
isInsideMultiHandle so it's safe if the user's end/error handler
closed the handle inside the deferred window (the curly pool relies
on this).

Reproduced reliably on node:22-alpine3.22 + libcurl 8.17 with 36k+
concurrent requests: before the fix ~73% failed with the recursive
error; after the fix all 36800 completed cleanly.

Fixes JCMais#439
Add a `test:stress` vitest config + script and a separate stress-test
GitHub Actions workflow that runs it on alpine and ubuntu after building
the addon from source. The current stress test reproduces JCMais#439: 40
concurrent requests every 20ms for 10s through a real multi handle,
counting any CURLM_RECURSIVE_API_CALL (libcurl code 8) as failure.

Without the perform() removeHandle deferral, alpine would fail roughly
3 out of every 4 requests. With it, the same load completes cleanly.
This is the canary — if a future change re-introduces a synchronous
curl_multi_* call from inside the notify/socket callback chain, this
job catches it before users do.

Kept separate from the main test suite because it's timing-sensitive
and runs for ~10s of pure load; gating every PR on it inside the normal
matrix would noise things up.
`wget -qO- | tar xzf -` can't recover from a half-finished download —
the partial bytes are already in tar's stdin by the time wget's retry
logic could fire, and tar bails with "gzip: stdin: unexpected end of
file" before anything restarts. We've seen this hit one cell of a
22-cell matrix run on libcurl 8.17.0's tarball (run 26722785133), with
the other 21 cells succeeding on the same input — classic curl.se /
GitHub-releases flake.

Stage the download to a temp file first so wget can retry until the
file is whole, then untar from the file. Also pass --tries, --timeout
and --retry-connrefused so transient network errors don't fall through
to a hard failure on the first attempt.

This is also incidentally happening as I write this commit — curl.se is
serving the tarball at ~1MB/s today, which would make the previous
script's per-attempt timeout window much more likely to clip.
The GNU wget retry flags I added in the previous commit (--tries,
--waitretry, --timeout, --retry-connrefused) are not supported by
BusyBox wget, which is what the Alpine container image ships. Every
alpine matrix cell on run 26723340409 died immediately at the first
download with "wget: unrecognized option: waitretry=3".

Hand-roll the retry loop in bash so we only need flags that work in
both GNU and BusyBox wget (-q, -O, -U, -T). Same retry count, same
backoff, same temp-file staging — just doesn't lean on wget for the
retry semantics anymore.

Verified locally on macOS (GNU wget 1.21.3) and on
node:25-alpine3.22 (BusyBox wget) — both extract zlib's v1.3.1
tarball cleanly.
pnpm v10 no longer runs dependency install scripts by default — every
package that needs preinstall/install/postinstall now has to be on the
consumer's allow-list. The windows-consumer-install workflow saw the
exact symptom: `pnpm add` reported "Ignored build scripts: node-libcurl"
and the install "succeeded", but vcpkg never ran, so the curl.exe sanity
check threw "vcpkg curl.exe was not found after install".

Opt in two ways so both invocations and any followup script behave the
same: the consumer package.json now declares
`pnpm.onlyBuiltDependencies = ["node-libcurl"]`, and the `pnpm add`
command also passes `--allow-build node-libcurl` (belt-and-braces for
the one-shot install).

This is the same hoop a real pnpm-v10 user has to jump through — so
keeping the workaround in the workflow rather than hiding it behind a
custom action means the CI continues to reflect what consumers
actually experience.
node-libcurl Bot and others added 12 commits June 6, 2026 19:09
Node.js 26 was built with clang-cl + lld and ThinLTO enabled. Its
installed common.gypi propagates `enable_thin_lto=true` to downstream
native addons, and the conditions inside common.gypi append
`-flto=thin` to cl.exe's AdditionalOptions and `/opt:lldltojobs=<n>`
to link.exe's AdditionalOptions whenever that variable is true.

That's fine when the addon is also built with clang-cl, but the
overwhelmingly common case for consumers is the regular MSVC toolchain
where:

  - cl.exe warns and ignores `-flto=thin` (D9002)
  - link.exe warns and ignores `/flto=thin` (LNK4044)
  - link.exe fails with `LNK1117: syntax error in option
    'opt:lldltojobs=2'` because /OPT: only accepts
    REF/ICF/NOREF/NOICF/LBR/NOLBR

Force enable_lto and enable_thin_lto off in binding.gyp's `variables`
so the common.gypi conditions evaluate false and the flags are never
emitted. Affects every consumer building node-libcurl from source on
Windows against Node 26; harmless on Linux/macOS and on older Node
versions.
The previous attempt at this fix (a222989) put `enable_lto: false` and
`enable_thin_lto: false` in binding.gyp's `variables` block. That didn't
actually work — node-gyp's create-config-gypi.js seeds config.gypi from
`process.config` of the Node binary running the build, and the resulting
config.gypi lives in a separate gyp scope from binding.gyp. binding.gyp
variables can't override config.gypi variables that way.

The real path: set npm_config_enable_thin_lto/enable_lto in the build
environment. node-gyp loops through unrecognized npm_config_* opts and
forwards them as `-D<name>=<value>` gyp defines, which take precedence
over both config.gypi and binding.gyp.

Wire that into scripts/ci/windows/build.ps1 so the build matrix runs are
fixed. The binding.gyp change is reverted since it wasn't doing anything.

This still leaves end-user source installs (e.g. consumers pnpm-adding
node-libcurl on Node 26 Windows with no prebuilt available) broken —
those users would need to set the same env vars themselves. A follow-up
should set GYP_DEFINES from our preinstall script so the workaround
applies transparently for them too.
…po-master-branch

chore: update with released code from upstream repo, master branch
@aborovsky aborovsky self-assigned this Jul 3, 2026
@aborovsky
aborovsky merged commit eb3c004 into master Jul 3, 2026
63 of 68 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants