Conversation
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.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.
…-upstream-repo-develop-branch
…ant state tracking
…po-master-branch chore: update with released code from upstream repo, master branch
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.