Commit 74564d94df5 for nodejs
commit 74564d94df5b6c0879efc5f6d1cdec23f39b1942
Author: Filip Skokan <panva.ip@gmail.com>
Date: Sun Sep 27 19:02:12 2026 +0200
worker: serialize messages after exit
The outside port must serialize and transfer messages even when it has
no peer. Retain that port after the backing thread exits and create a
closed port when the entry script fetch fails. This preserves clone
errors and transfer side effects in both cases.
Signed-off-by: Filip Skokan <panva.ip@gmail.com>
Assisted-by: Codex
PR-URL: https://github.com/nodejs/node/pull/66354
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Aviv Keller <me@aviv.sh>
diff --git a/lib/internal/webworker.js b/lib/internal/webworker.js
index cf5f1df6372..744fe4de068 100644
--- a/lib/internal/webworker.js
+++ b/lib/internal/webworker.js
@@ -68,6 +68,7 @@ const {
} = require('internal/worker');
const {
+ MessageChannel,
lazyMessageEvent,
} = require('internal/worker/io');
@@ -107,6 +108,7 @@ const kLocation = Symbol('kLocation');
const kName = Symbol('kName');
const kNavigator = Symbol('kNavigator');
const kNavigatorBrand = Symbol('kNavigatorBrand');
+const kOutsidePort = Symbol('kOutsidePort');
const kType = Symbol('kType');
const kURL = Symbol('kURL');
const kWorker = Symbol('kWorker');
@@ -808,6 +810,12 @@ class Worker extends EventTarget {
this[kWorker] = null;
const entry = resolveWorkerEntry(workerURL);
if (entry === null) {
+ // Even a failed worker has an outside port. Keep a closed port so
+ // postMessage() still serializes its message and transfers objects.
+ const { port1, port2 } = new MessageChannel();
+ port1.close();
+ port2.close();
+ this[kOutsidePort] = port1;
// "If the algorithm asynchronously completes with null or with a
// script whose error to rethrow is non-null, then: Queue a global
// task on the DOM manipulation task source given worker's relevant
@@ -837,7 +845,8 @@ class Worker extends EventTarget {
},
});
- forwardMessageEvents(this[kWorker][kPublicPort], this);
+ this[kOutsidePort] = this[kWorker][kPublicPort];
+ forwardMessageEvents(this[kOutsidePort], this);
// "Set notHandled to the result of firing an event named error at
// workerObject, using ErrorEvent, with the cancelable attribute
@@ -872,7 +881,7 @@ class Worker extends EventTarget {
// invoked the respective postMessage(message, transfer) and
// postMessage(message, options) on this's outside port, with the same
// arguments, and returned the same return value."
- this[kWorker]?.postMessage(message, transfer);
+ this[kOutsidePort].postMessage(message, transfer);
}
// The following properties are non-standard, Node.js extensions
diff --git a/test/parallel/test-webworker-postmessage-lifecycle.js b/test/parallel/test-webworker-postmessage-lifecycle.js
new file mode 100644
index 00000000000..51acd57fa62
--- /dev/null
+++ b/test/parallel/test-webworker-postmessage-lifecycle.js
@@ -0,0 +1,30 @@
+// Flags: --experimental-web-worker
+'use strict';
+
+const common = require('../common');
+const assert = require('node:assert');
+
+function checkSerialization(worker) {
+ assert.throws(() => worker.postMessage(() => {}), { name: 'DataCloneError' });
+ const buffer = new ArrayBuffer(8);
+ assert.throws(() => worker.postMessage(null, [buffer, buffer]), { name: 'DataCloneError' });
+ assert.strictEqual(buffer.byteLength, 8);
+ worker.postMessage(null, [buffer]);
+ assert.strictEqual(buffer.byteLength, 0);
+}
+
+// A failed script fetch still leaves an outside port that serializes messages.
+{
+ const worker = new Worker('data:text/plain,');
+ worker.onerror = common.mustCall(() => checkSerialization(worker));
+ checkSerialization(worker);
+}
+
+// Serialization is required even after the backing thread has exited.
+{
+ const worker = new Worker('data:text/javascript,close()');
+ worker.onerror = common.mustNotCall('worker failed');
+ process.once('worker', common.mustCall((thread) => {
+ thread.once('exit', common.mustCall(() => checkSerialization(worker)));
+ }));
+}