diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 1b6c212c4b75..533eff2d7003 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -661,7 +661,9 @@ declare module "bun" { ref?: boolean; /** - * In Bun, this does nothing. + * `"classic"` or `"module"`. Any other value throws a `TypeError`, as in browsers. + * + * Otherwise this has no effect in Bun: every worker runs as an ES module. */ type?: Bun.WorkerType | undefined; @@ -683,7 +685,9 @@ declare module "bun" { env?: Record | (typeof import("node:worker_threads"))["SHARE_ENV"] | undefined; /** - * In Bun, this does nothing. + * `"omit"`, `"same-origin"` or `"include"`. Any other value throws a `TypeError`, as in browsers. + * + * Otherwise this has no effect in Bun: worker scripts are not fetched with credentials. */ credentials?: import("undici-types").RequestCredentials | undefined; diff --git a/src/jsc/bindings/webcore/JSWorker.cpp b/src/jsc/bindings/webcore/JSWorker.cpp index b94283911342..54a6f80ed683 100644 --- a/src/jsc/bindings/webcore/JSWorker.cpp +++ b/src/jsc/bindings/webcore/JSWorker.cpp @@ -134,6 +134,22 @@ STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSWorkerPrototype, JSWorkerPrototype::Base); using JSWorkerDOMConstructor = JSDOMConstructor; +// A WebIDL enumeration member of WorkerOptions: absent or undefined passes, anything else must +// stringify to one of `allowed` or a TypeError is thrown and false returned. +static bool validateEnumerationOption(JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& throwScope, JSValue value, ASCIILiteral name, std::span allowed) +{ + if (!value || value.isUndefined()) + return true; + auto string = value.toWTFString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, false); + for (auto literal : allowed) { + if (string == literal) + return true; + } + Bun::ERR::INVALID_ARG_VALUE(throwScope, lexicalGlobalObject, name, "must be one of: "_s, value, allowed); + return false; +} + template<> __attribute__((minsize)) JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSWorkerDOMConstructor::construct(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame) { auto& vm = JSC::getVM(lexicalGlobalObject); @@ -183,6 +199,21 @@ template<> __attribute__((minsize)) JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES } } + // Valid values have no further effect (every worker is an ES module, nothing is fetched with + // credentials). node:worker_threads has neither option and keeps ignoring them. + if (options.kind == WorkerOptions::Kind::Web) { + static constexpr ASCIILiteral workerTypes[] = { "classic"_s, "module"_s }; + static constexpr ASCIILiteral requestCredentials[] = { "omit"_s, "same-origin"_s, "include"_s }; + auto typeValue = optionsObject->getIfPropertyExists(lexicalGlobalObject, vm.propertyNames->type); + RETURN_IF_EXCEPTION(throwScope, {}); + if (!validateEnumerationOption(lexicalGlobalObject, throwScope, typeValue, "options.type"_s, workerTypes)) + return {}; + auto credentialsValue = optionsObject->getIfPropertyExists(lexicalGlobalObject, Identifier::fromString(vm, "credentials"_s)); + RETURN_IF_EXCEPTION(throwScope, {}); + if (!validateEnumerationOption(lexicalGlobalObject, throwScope, credentialsValue, "options.credentials"_s, requestCredentials)) + return {}; + } + auto miniModeValue = optionsObject->getIfPropertyExists(lexicalGlobalObject, Identifier::fromString(vm, "smol"_s)); RETURN_IF_EXCEPTION(throwScope, {}); if (miniModeValue) { diff --git a/test/js/web/workers/worker.test.ts b/test/js/web/workers/worker.test.ts index 102b6c6f7dbd..99e9f71eafd1 100644 --- a/test/js/web/workers/worker.test.ts +++ b/test/js/web/workers/worker.test.ts @@ -384,6 +384,47 @@ describe("web worker", () => { }); }); + // WebIDL: WorkerOptions.type is a WorkerType and .credentials a RequestCredentials enumeration, so + // any other value is a TypeError at construction. Valid values have no further effect in Bun. + describe("type and credentials options", () => { + test.each([ + [{ type: "zzz" }, "options.type"], + [{ type: null }, "options.type"], + [{ type: 1 }, "options.type"], + [{ credentials: "zzz" }, "options.credentials"], + ])("new Worker(url, %j) throws a TypeError", (options, name) => { + let error: any; + try { + new Worker("data:text/javascript,", options as any); + } catch (e) { + error = e; + } + expect(error).toBeInstanceOf(TypeError); + expect(error.message).toStartWith(`The property '${name}' must be one of:`); + }); + + test("valid or undefined values are accepted", async () => { + for (const options of [ + { type: "module" }, + { type: "classic" }, + { type: undefined }, + { credentials: "omit" }, + { credentials: "same-origin" }, + { credentials: "include" }, + ] satisfies WorkerOptions[]) { + const worker = new Worker("data:text/javascript,", options); + await once(worker, "close"); + } + }); + + // node:worker_threads has neither option. Unknown keys are ignored there, as in Node. + test("worker_threads.Worker does not validate them", async () => { + const worker = new wt.Worker(new URL("data:text/javascript,"), { type: "zzz", credentials: "zzz" } as any); + const [code] = await once(worker, "exit"); + expect(code).toBe(0); + }); + }); + describe("error event", () => { test("is fired with a string of the error", async () => { const worker = new Worker("data:text/javascript,throw 5"); @@ -679,6 +720,8 @@ describe("web worker", () => { // after the request must release, not build script values under it. test("terminate() while fs.readFile completions keep arriving", async () => { using dir = tempDir("worker-readfile-churn", { "f.bin": Buffer.alloc(65536, 7) }); + // Each round boots 4 workers; a debug build spends ~0.5s per round, so it runs fewer. + const rounds = isDebug ? 4 : 12; await using proc = Bun.spawn({ cmd: [ bunExe(), @@ -687,7 +730,7 @@ describe("web worker", () => { let n = 0; (function pump(){ while (n < 16) { n++; readFile(\${JSON.stringify(process.argv[1])}, () => { n--; setImmediate(pump) }) } })(); postMessage("busy")\`; const url = URL.createObjectURL(new Blob([src])); - for (let r = 0; r < 12; r++) await Promise.all(Array.from({ length: 4 }, (_, i) => new Promise(res => { + for (let r = 0; r < ${rounds}; r++) await Promise.all(Array.from({ length: 4 }, (_, i) => new Promise(res => { const w = new Worker(url); w.addEventListener("close", res); w.onmessage = () => setTimeout(() => w.terminate(), (r + i) % 10) }))); console.log("PASS");`, path.join(String(dir), "f.bin"),