Follow what changed in Node.js between 26.3.0 and 26.12.1

Five test files use the real `node` as a reference or as the server, and
failed once the CI machines had 26.12.1. Each was traced to the release that
changed the behavior.

- Node-API (26.12): `node_api_create_object_with_properties` takes
  `const napi_value*`. Bun's copy of the header and its definition follow, so
  the three headers are byte-identical to upstream again. The test addon no
  longer declares the function itself: the header does, in either form.
- node:http2 (26.11.0): the server entry points check
  `strictSingleValueFields` before `unknownProtocolTimeout`. Bun does too.
- TLS renegotiation (26.10.0): the request of `renegotiate()` leaves ahead of
  a `write()` that has not finished. The fixture relied on the old order and
  now waits for the write. `fetch` was right to refuse.
- TLS sessions (26.3.1, a security release): a session is only offered to the
  host it came from. The fixture names the host on every path. node's
  `TLSSocket` constructor does not read its own new format, so that cell
  accepts either answer under node and stays strict under Bun.
- node:http (26.7.0): `writableFinished` says whether 'finish' was emitted.
  Bun's is unchanged, and the test says where the two differ.

The macOS test machines keep an older node until they are baked again, so the
two reference runs whose expected output changed are skipped below the release
that changed it.
This commit is contained in:
Claude committed 2026-10-23 00:01:28 +00:00
1 parent b351a7326b
commit 3c1e4e0275
9 files changed
+35 -24

No files matched your search

+4 -4
View File
@@ -6543,6 +6543,10 @@ function initializeOptions(options) {
if (options.remoteCustomSettings.length > MAX_ADDITIONAL_SETTINGS) throw $ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS();
}
if (options.strictSingleValueFields !== undefined) {
validateBoolean(options.strictSingleValueFields, "options.strictSingleValueFields");
}
if (options.maxSessionInvalidFrames !== undefined)
validateUint32(options.maxSessionInvalidFrames, "options.maxSessionInvalidFrames");
@@ -6554,10 +6558,6 @@ function initializeOptions(options) {
validateUint32(options.unknownProtocolTimeout, "options.unknownProtocolTimeout");
else options.unknownProtocolTimeout = 10000;
if (options.strictSingleValueFields !== undefined) {
validateBoolean(options.strictSingleValueFields, "options.strictSingleValueFields");
}
// Initialize http1Options bag for HTTP/1 fallback when allowHTTP1 is true.
options.http1Options = { ...options.http1Options };
if (options.Http1IncomingMessage !== undefined) {
+2 -2
View File
@@ -1616,8 +1616,8 @@ extern "C" JS_EXPORT napi_status node_api_set_prototype(napi_env env,
extern "C" JS_EXPORT napi_status node_api_create_object_with_properties(napi_env env,
napi_value prototype_or_null,
napi_value* property_names,
napi_value* property_values,
const napi_value* property_names,
const napi_value* property_values,
size_t property_count,
napi_value* result)
{
+2 -2
View File
@@ -55,8 +55,8 @@ NAPI_EXTERN napi_status NAPI_CDECL napi_create_object(napi_env env,
NAPI_EXTERN napi_status NAPI_CDECL
node_api_create_object_with_properties(napi_env env,
napi_value prototype_or_null,
napi_value* property_names,
napi_value* property_values,
const napi_value* property_names,
const napi_value* property_values,
size_t property_count,
napi_value* result);
#endif // NAPI_EXPERIMENTAL
@@ -290,7 +290,8 @@ for (const path of ["the listener of the server", "emit('connection')"]) {
events: ["prefinish"],
finished: true,
writableEnded: true,
writableFinished: true,
// Since v26.7.0 node's says whether 'finish' was emitted. Bun's still says that nothing is left to write.
writableFinished: typeof Bun !== "undefined",
});
});
}
@@ -400,7 +401,13 @@ for (const expectation of ["100-continue", "something-else"]) {
// Only in Bun: when Node.js runs this file it must not spawn itself again.
if (typeof Bun !== "undefined") {
const { bunEnv, nodeExe } = await import("harness");
const node = nodeExe();
// Before v26.7.0 node's writableFinished did not wait for 'finish', and the macOS CI hosts can have an older node.
const node = (() => {
const node = nodeExe();
if (!node) return null;
const { stdout } = Bun.spawnSync({ cmd: [node, "-p", "process.versions.node"], env: bunEnv, stderr: "ignore" });
return Bun.semver.order(stdout.toString().trim(), "26.7.0") >= 0 ? node : null;
})();
describe("Node.js compatibility", () => {
(node ? test : test.skip)("all tests pass in Node.js", async () => {
+3 -5
View File
@@ -5021,9 +5021,7 @@ const optionChecksExpected = `{ strictSingleValueFields: "yes" }
connect(https), createServer, createSecureServer, performServerHandshake
{ unknownProtocolTimeout: -1, strictSingleValueFields: "yes" }
TypeError [ERR_INVALID_ARG_TYPE]: The "options.strictSingleValueFields" property must be of type boolean. Received type string ('yes')
connect(http), connect(https), connect(createConnection)
RangeError [ERR_OUT_OF_RANGE]: The value of "options.unknownProtocolTimeout" is out of range. It must be >= 0 && <= 4294967295. Received -1
createServer, createSecureServer, performServerHandshake
every entry point
other calls
TypeError [ERR_INVALID_ARG_TYPE]: The "options.strictSingleValueFields" property must be of type boolean. Received type string ('yes')
connect("not a url", { strictSingleValueFields: "yes" })
@@ -5044,12 +5042,12 @@ other calls
rejects with RangeError [ERR_OUT_OF_RANGE]: The value of "options.maxSessionInvalidFrames" is out of range. It must be >= 0 && <= 4294967295. Received -1
promisify(connect)(https url, { maxSessionInvalidFrames: -1 })
`;
// An older node has no strictSingleValueFields option, and the macOS CI hosts can have one.
// Before v26.11.0 node checked the options in another order, and the macOS CI hosts can have an older node.
const optionChecksNode = (() => {
const node = nodeExe();
if (!node) return null;
const { stdout } = Bun.spawnSync({ cmd: [node, "-p", "process.versions.node"], env: bunEnv, stderr: "ignore" });
return parseInt(stdout.toString(), 10) >= 26 ? node : null;
return Bun.semver.order(stdout.toString().trim(), "26.11.0") >= 0 ? node : null;
})();
for (const [name, runtime] of [
+4 -2
View File
@@ -303,7 +303,9 @@ async function serverWithSession() {
export async function session() {
const { server, port, session, connected } = await serverWithSession();
const host = "127.0.0.1";
const options = { session, rejectUnauthorized: false };
// Since Node.js 26.3.1 a session is only offered to the host it came from. A socket that is already connected
// does not say which that is, so every path names it.
const options = { session, host, rejectUnauthorized: false };
// The listener is attached in the turn that makes the socket: a wrap starts its handshake at once.
async function reused(socket, event, prepare) {
try {
@@ -317,7 +319,7 @@ export async function session() {
}
try {
return {
"tls.connect({ port, session })": await reused(tls.connect({ port, host, ...options }), "secureConnect"),
"tls.connect({ port, session })": await reused(tls.connect({ port, ...options }), "secureConnect"),
"tls.connect({ socket, session })": await reused(
tls.connect({ socket: await connected(), ...options }),
"secureConnect",
+5 -3
View File
@@ -5125,7 +5125,7 @@ describe("new tls.TLSSocket(socket) on the client side", () => {
});
// Bun runs the functions of the fixture in this process. Node runs the same file as a
// script, so each expected report below is also node v26.3.0's.
// script, so each expected report below is also node v26.12.1's.
async function underNode(mode: string) {
await using proc = Bun.spawn({
cmd: [nodeExe()!, join(import.meta.dir, "node-tls-client-wrap-fixture.mjs"), mode],
@@ -5143,7 +5143,7 @@ describe("new tls.TLSSocket(socket) on the client side", () => {
describe.concurrent.each([
["bun", false, clientWrap.shutdown, clientWrap.mysql, clientWrap.peerCloses, clientWrap.session],
["node", !nodeExe(), onNode("shutdown"), onNode("mysql"), onNode("peer-closes"), onNode("session")],
] as const)("under %s", (_runtime, skip, shutdown, mysql, peerCloses, session) => {
] as const)("under %s", (runtime, skip, shutdown, mysql, peerCloses, session) => {
// Each cell calls the method and then destroy(): end() alone leaves a wrap open, on node too.
it.skipIf(skip)(
"end(), end(cb), destroySoon() and destroy() do not throw, and destroy() closes the wrap",
@@ -5178,7 +5178,9 @@ describe("new tls.TLSSocket(socket) on the client side", () => {
expect(await session()).toEqual({
"tls.connect({ port, session })": true,
"tls.connect({ socket, session })": true,
"new TLSSocket(socket, { session })": true,
// Since v26.3.1 node's getSession() puts the host in front of the session, and its constructor is the one
// place that does not take it off again. The macOS CI hosts can have an older node.
"new TLSSocket(socket, { session })": runtime === "bun" ? true : expect.any(Boolean),
"tls.connect({ port }), then setSession()": true,
});
});
+6 -1
View File
@@ -476,8 +476,13 @@ const backToBackRenegotiationServer = /* js */ `
socket.resume();
const responseFirst = head.startsWith("GET /response-first ");
let asked = 0;
let answered = false;
(function ask() {
if (asked === 3 && responseFirst) socket.write("HTTP/1.1 200 OK\\r\\nContent-Length: 2\\r\\n\\r\\nok");
// Since Node.js 26.10 the request of a renegotiate() leaves ahead of a write() that has not finished.
if (asked === 3 && responseFirst && !answered) {
answered = true;
return socket.write("HTTP/1.1 200 OK\\r\\nContent-Length: 2\\r\\n\\r\\nok", ask);
}
if (asked === 4) {
if (isHttp) socket.end("HTTP/1.1 200 OK\\r\\nContent-Length: 2\\r\\nConnection: close\\r\\n\\r\\nok");
return;
-3
View File
@@ -3968,9 +3968,6 @@ test_napi_adjust_external_memory(const Napi::CallbackInfo &info) {
extern "C" {
napi_status node_api_set_prototype(napi_env env, napi_value object,
napi_value value);
napi_status node_api_create_object_with_properties(
napi_env env, napi_value prototype_or_null, napi_value *property_names,
napi_value *property_values, size_t property_count, napi_value *result);
napi_status node_api_is_sharedarraybuffer(napi_env env, napi_value value,
bool *result);
napi_status node_api_create_sharedarraybuffer(napi_env env, size_t byte_length,