From 37608933ae06e8d140317b191e2436dac3faf93a Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Thu, 20 Jun 2024 17:01:50 +0200 Subject: [PATCH] fix: fetch-dependent interfaces in Web Workers (#42579) --- lib/worker/init.ts | 8 ++++++ shell/renderer/web_worker_observer.cc | 30 ++++++++++++-------- spec/chromium-spec.ts | 25 ++++++++++++++++- spec/fixtures/pages/worker-fetch.html | 12 ++++++++ spec/fixtures/workers/worker_node_fetch.js | 7 +++++ spec/node-spec.ts | 32 ++++++++++++++++++++++ 6 files changed, 101 insertions(+), 13 deletions(-) create mode 100644 spec/fixtures/pages/worker-fetch.html create mode 100644 spec/fixtures/workers/worker_node_fetch.js diff --git a/lib/worker/init.ts b/lib/worker/init.ts index 0c92d61e88..d23a4f942b 100644 --- a/lib/worker/init.ts +++ b/lib/worker/init.ts @@ -17,6 +17,14 @@ const { makeRequireFunction } = __non_webpack_require__('internal/modules/helper global.module = new Module('electron/js2c/worker_init'); global.require = makeRequireFunction(global.module); +// See WebWorkerObserver::WorkerScriptReadyForEvaluation. +if ((globalThis as any).blinkfetch) { + const keys = ['fetch', 'Response', 'FormData', 'Request', 'Headers']; + for (const key of keys) { + (globalThis as any)[key] = (globalThis as any)[`blink${key}`]; + } +} + // Set the __filename to the path of html file if it is file: protocol. // NB. 'self' isn't defined in an AudioWorklet. if (typeof self !== 'undefined' && self.location.protocol === 'file:') { diff --git a/shell/renderer/web_worker_observer.cc b/shell/renderer/web_worker_observer.cc index 0faf53b1c5..eafa8ba00a 100644 --- a/shell/renderer/web_worker_observer.cc +++ b/shell/renderer/web_worker_observer.cc @@ -66,9 +66,25 @@ void WebWorkerObserver::WorkerScriptReadyForEvaluation( std::shared_ptr env = node_bindings_->CreateEnvironment(worker_context, nullptr); + // We need to use the Blink implementation of fetch in web workers + // Node.js deletes the global fetch function when their fetch implementation + // is disabled, so we need to save and re-add it after the Node.js environment + // is loaded. See corresponding change in node/init.ts. v8::Local global = worker_context->Global(); - v8::Local fetch_string = gin::StringToV8(env->isolate(), "fetch"); - v8::MaybeLocal fetch = global->Get(worker_context, fetch_string); + + std::vector keys = {"fetch", "Response", "FormData", "Request", + "Headers"}; + for (const auto& key : keys) { + v8::MaybeLocal value = + global->Get(worker_context, gin::StringToV8(isolate, key.c_str())); + if (!value.IsEmpty()) { + std::string blink_key = "blink" + key; + global + ->Set(worker_context, gin::StringToV8(isolate, blink_key.c_str()), + value.ToLocalChecked()) + .Check(); + } + } // Add Electron extended APIs. electron_bindings_->BindTo(env->isolate(), env->process_object()); @@ -76,16 +92,6 @@ void WebWorkerObserver::WorkerScriptReadyForEvaluation( // Load everything. node_bindings_->LoadEnvironment(env.get()); - // We need to use the Blink implementation of fetch in WebWorker process - // Node.js deletes the global fetch function when their fetch implementation - // is disabled, so we need to save and re-add it after the Node.js environment - // is loaded. - if (!fetch.IsEmpty()) { - worker_context->Global() - ->Set(worker_context, fetch_string, fetch.ToLocalChecked()) - .Check(); - } - // Make uv loop being wrapped by window context. node_bindings_->set_uv_env(env.get()); diff --git a/spec/chromium-spec.ts b/spec/chromium-spec.ts index 9431056bde..238480364c 100644 --- a/spec/chromium-spec.ts +++ b/spec/chromium-spec.ts @@ -1019,12 +1019,35 @@ describe('chromium features', () => { }); it('Worker has node integration with nodeIntegrationInWorker', async () => { - const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, nodeIntegrationInWorker: true, contextIsolation: false } }); + const w = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + nodeIntegrationInWorker: true, + contextIsolation: false + } + }); + w.loadURL(`file://${fixturesPath}/pages/worker.html`); const [, data] = await once(ipcMain, 'worker-result'); expect(data).to.equal('object function object function'); }); + it('Worker has access to fetch-dependent interfaces with nodeIntegrationInWorker', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + nodeIntegrationInWorker: true, + contextIsolation: false + } + }); + + w.loadURL(`file://${fixturesPath}/pages/worker-fetch.html`); + const [, data] = await once(ipcMain, 'worker-fetch-result'); + expect(data).to.equal('function function function function function'); + }); + describe('SharedWorker', () => { it('can work', async () => { const w = new BrowserWindow({ show: false }); diff --git a/spec/fixtures/pages/worker-fetch.html b/spec/fixtures/pages/worker-fetch.html new file mode 100644 index 0000000000..f573784293 --- /dev/null +++ b/spec/fixtures/pages/worker-fetch.html @@ -0,0 +1,12 @@ + + + + + diff --git a/spec/fixtures/workers/worker_node_fetch.js b/spec/fixtures/workers/worker_node_fetch.js new file mode 100644 index 0000000000..0f6ca70f04 --- /dev/null +++ b/spec/fixtures/workers/worker_node_fetch.js @@ -0,0 +1,7 @@ +self.postMessage([ + typeof fetch, + typeof Response, + typeof Request, + typeof Headers, + typeof FormData +].join(' ')); diff --git a/spec/node-spec.ts b/spec/node-spec.ts index f146d077a0..9c5059172c 100644 --- a/spec/node-spec.ts +++ b/spec/node-spec.ts @@ -159,6 +159,38 @@ describe('node feature', () => { }); }); + describe('fetch', () => { + itremote('works correctly when nodeIntegration is enabled in the renderer', async (fixtures: string) => { + const file = require('node:path').join(fixtures, 'hello.txt'); + expect(() => { + fetch('file://' + file); + }).to.not.throw(); + + expect(() => { + const formData = new FormData(); + formData.append('username', 'Groucho'); + }).not.to.throw(); + + expect(() => { + const request = new Request('https://example.com', { + method: 'POST', + body: JSON.stringify({ foo: 'bar' }) + }); + expect(request.method).to.equal('POST'); + }).not.to.throw(); + + expect(() => { + const response = new Response('Hello, world!'); + expect(response.status).to.equal(200); + }).not.to.throw(); + + expect(() => { + const headers = new Headers(); + headers.append('Content-Type', 'text/xml'); + }).not.to.throw(); + }, [fixtures]); + }); + it('does not hang when using the fs module in the renderer process', async () => { const appPath = path.join(mainFixturesPath, 'apps', 'libuv-hang', 'main.js'); const appProcess = childProcess.spawn(process.execPath, [appPath], {