Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cross-origin Web Workers still require worker-loader #16696

Closed
kirill-konshin opened this issue Feb 3, 2023 · 9 comments
Closed

Cross-origin Web Workers still require worker-loader #16696

kirill-konshin opened this issue Feb 3, 2023 · 9 comments

Comments

@kirill-konshin
Copy link

kirill-konshin commented Feb 3, 2023

Bug report

What is the current behavior?

It's known that Web Workers must be served from same origin as the page itself. Seems that even Content-Security-Policy: worker-src http://xxx has no effect on this.

We can use worker-loader to create a blob instead of a worker file.

BUT documentation https://webpack.js.org/loaders/worker-loader/ says:

DEPRECATED for v5: https://webpack.js.org/guides/web-workers/

The problem is, despite deprecation, this is the only way for now to overcome cross origin issues, documentation does not say anything about cross domain.

There are various solutions, but none of them work properly with Webpack...

If the current behavior is a bug, please provide the steps to reproduce.

I have put together the repo: https://github.com/kirill-konshin/cross-domain-worker/tree/original-issue, clone, run npm install and npm start, then just open http://localhost:3000 and check console. The demo exhibits following:

2 servers, static HTML on :3000 and Webpack Dev Server on :4001.

Page served from :3000:

<html>
  <script src="http://localhost:4001/entry.js"></script>
</html>

JS served from :4001:

// entry.js
const worker = new Worker(new URL('./worker', import.meta.url));

// worker.js
console.log('Foo');

If you open the http://localhost:3000, you'll get:

Uncaught DOMException: Failed to construct 'Worker': Script at 'http://localhost:4000/test/src_worker_js.js' cannot be accessed from origin 'http://localhost:3000'.

Documentation does not suggest any solution for this.

What is the expected behavior?

Two options:

  1. Provide a good example how to make it work using modern approach or
  2. Un-deprecate worker-loader and provide documentation when to use it, and hopefully fix the additional issue

Additional issue

If 2nd approach will be taken, I'd like to report an additional problem, but the loader repo is archived, so I'll put it here, with the workaround.

Since worker-loader will create a blob, it has no access to proper __webpack_public_path__, in worker it will be blob:http://localhost:3000/, e.g. host origin, so worker loaded this way won't have access to assets.

I've assembled a short demo, where I added devServer.proxy which can serve same content from /test/ and / to mimic how content can be served from different CDNs and paths using output.publicPath='auto'.

Solution is to postMessage proper __webpack_public_path__ from main thread, and then wrap all asset requests in the worker:

// entry.js
const worker = new Worker('./worker', import.meta.url);
worker.postMessage({msg: 'base', payload: __webpack_public_path__});

// worker.js
function joinSegments(a, b) {
    return '/'   a.split('/').concat(b.split('/')).filter(Boolean).join('/');
}

function replaceOrigin(blob, base) {
    const url = new URL(blob.replace('blob:', ''));
    const search = url.searchParams.toString();
    const baseUrl = new URL(base);
    return new URL(joinSegments(baseUrl.pathname, url.pathname)   (search ? '?'   search : ''), baseUrl.origin).toString();
}

self.onmessage = ({data: {msg, payload}}) => {
    if (msg === 'base') {
        const image = require('./package.png');
        console.log(payload); // returns http://localhost:4001/test/ which is right
        console.log(image); // returns blob:http://localhost:3000/xxx.png, should be localhost:4001
        console.log(__webpack_public_path__); // returns blob:http://localhost:3000/, should be http://localhost:4001/test/

        const imageProper = replaceOrigin(image, payload);
        console.log(imageProper);
        fetch(imageProper).then(r => console.log(r));
    }
};

Very awkward, but works well.

Hopefully others can either copy-paste the hack, or, fingers crossed, worker-loader can be revived and can provide some normal way to recover dynamic public paths.

@blksky
Copy link

blksky commented Feb 21, 2023

In Webpack5 We need a universal solution!! Is there a god who knows?

@blksky
Copy link

blksky commented Feb 21, 2023

How do I load web workers across domains in Webpack5? Does Webpack5 support inline workers?

@alexander-akait
Copy link
Member

Sorry for delay, we don't create blob by default because it will increase your bundle, you need to create Blob on application side and handle it there, I provide solutions multiple times with different examples and how to handle them, look at dicussions, worker-loader is depcreated and should not be used anymore.

For worker public path we have #16671 and it will be in the next release.

Uncaught DOMException: Failed to construct 'Worker': Script at 'http://localhost:4000/test/src_worker_js.js' cannot be accessed from origin 'http://localhost:3000'.

It should be handled and configured on server side, yes, you can setup proxy for such purposes. Sorry but we can't do something here. Blob is good approach only for old browsers as a fallback.

Also you provided https://medium.com/@krishnachirumamilla/content-security-policy-worker-src-cd06ecfa2fe8, and there is solution, if you need such things you can use multi compiler mode and set target: 'webworker' and load your worker sciprt as your want - look at assets modules (raw loading), so you will get compiled code and pass it to Blob. You can write code based on your logic - blob for dev env and old browser and modern worker for new browsers.

feel free to feedback

If you need I can write you configuration to you but then I need an repository with your real problem, no need to provide whole code, anyway I strongly recommend to look at multi compoer mode with target: 'webworker' it is very flexibility for any purposes

@kirill-konshin
Copy link
Author

kirill-konshin commented Mar 22, 2023

Sorry for delay, we don't create blob by default because it will increase your bundle, you need to create Blob on application side and handle it there, I provide solutions multiple times with different examples and how to handle them, look at dicussions, worker-loader is depcreated and should not be used anymore.

Can you please point out the proper one. There are too many different discussions and advices, it would be great to get the right one from you, since you're the expert.

For worker public path we have #16671 and it will be in the next release.

Will it work same way as publicPath: auto, e.g. path/domain agnostic? I don't know which domain & path the build will be deployed at the time of compilation.

Uncaught DOMException: Failed to construct 'Worker': Script at 'http://localhost:4000/test/src_worker_js.js' cannot be accessed from origin 'http://localhost:3000'.

It should be handled and configured on server side, yes, you can setup proxy for such purposes. Sorry but we can't do something here. Blob is good approach only for old browsers as a fallback.

Also you provided https://medium.com/@krishnachirumamilla/content-security-policy-worker-src-cd06ecfa2fe8, and there is solution, if you need such things you can use multi compiler mode and set target: 'webworker' and load your worker sciprt as your want - look at assets modules (raw loading), so you will get compiled code and pass it to Blob. You can write code based on your logic - blob for dev env and old browser and modern worker for new browsers.

Author of the article says: "Browsers don’t allow you to create a worker with a URL pointing to a different domain.". Chrome ignores the Content-Security-Policy": "worker-src http://localhost:4000 (different domain) and still throws the above-mentioned error. It seems that you can only allow same-origin workers.

I have tried target: 'webworker' — no luck, since it's the problem of Chrome, not compilation. Thus, my statement about necessity to use blob even in modern browsers.

I have used following technique:

const myWorker = function createWorker(workerUrl) {
    const blob = new Blob([`importScripts('${workerUrl}');`], {'type': 'application/javascript'});
    return new Worker(URL.createObjectURL(blob));
};

Here's the repo: https://github.com/kirill-konshin/cross-domain-worker/tree/target-webworker (note the branch target-webworker). In order to see the error uncomment const worker = new Worker("http://localhost:4000/test/main.js") and comment const worker = myWorker("http://localhost:4000/test/main.js") in packages/host/src/index.js.

Unfortunately blob worker throws error when adding import('./image.png'): Uncaught (in promise) DOMException: Failed to execute 'importScripts' on 'WorkerGlobalScope': The script at 'blob:http://localhost:3000/src_image_png.js' failed to load. — note that it uses host URL, not worker URL, and tries to load image_png.js despite the asset/inline setting for PNG. It works with regular require('./image.png'). I should probably file another bug for this...

If you need I can write you configuration to you but then I need an repository with your real problem, no need to provide whole code, anyway I strongly recommend to look at multi compoer mode with target: 'webworker' it is very flexibility for any purposes

The whole code has been provided: https://github.com/kirill-konshin/cross-domain-worker/tree/original-issue, along with comments in the code and here.

@kirill-konshin
Copy link
Author

kirill-konshin commented Mar 22, 2023

Looks like I've solved most of the issues, the trick was to explicitly supply a true worker path as __webpack_public_path__ and __webpack_require__.p inside the worker. Still needs a small blob helper to overcome the same-origin policy.

What's supported:

  • cross-domain worker
  • no worker loader
  • sourcemaps
  • dynamic import() of images and anything else

https://github.com/kirill-konshin/cross-domain-worker/blob/main/packages/cross-domain-worker/src/index.js I have published the NPM package cross-domain-worker since the solution is quite reusable.

https://github.com/kirill-konshin/cross-domain-worker/blob/main/packages/host/src/index.js usage in main thread.

https://github.com/kirill-konshin/cross-domain-worker/blob/main/packages/worker/src/worker.js usage in worker.

@alexander-akait
Copy link
Member

Hello, sorry for delay, I see you have solved everything, my solution is similar (and it's somewhere here in the questions 😄), we also merge a PR to allow to public path for workers (so setPath can be removed if you really don't want to do it dynamically)

Do you help help with something else? Also I think we need document this

@gigadie
Copy link

gigadie commented Dec 5, 2023

The problem persists imho if we want to use this in combination with webpack transpiling of the worker.ts file.
What I mean is const worker = await createWorker("http://localhost:4000/worker.js"); it's considering the worker file is already transpiled as worker.js and served in the root folder. If we are using an Angular app for example, and we have our my.worker.ts file in the code-base, how do we get to figuring out the url http://localhost:4000/my.worker.js?

We are moving from a solution:

const ow = new Worker(
  new URL(
    '../workers/my.worker',
    import.meta.url,
  ),
  { type: 'module', name: 'my-worker' },
);

where webpack is able to transpile and output the js file, then modify here and replace the URL with the correct filePath, to this (using import { createWorker } from 'cross-domain-worker';):

const worker = await createWorker(
  new URL(
    '../workers/my.worker',
    import.meta.url,
  ).toString(), // cause the library only accepts strings
);

that doesn't work as expected, unless I'm missing some other configuration anywhere else.

@gigadie
Copy link

gigadie commented Dec 6, 2023

I ended up using fetch instead:

export class CorsWorker {
    private readonly url: string | URL;
    private readonly options?: WorkerOptions;

    // Have this only to trick Webpack into properly transpiling
    // the url address from the component which uses it
    constructor(url: string | URL, options?: WorkerOptions) {
        this.url = url;
        this.options = options;
    }

    async createWorker(): Promise<Worker> {
        const f = await fetch(this.url);
        const t = await f.text();
        const b = new Blob([t], {
            type: 'application/javascript',
        });
        const url = URL.createObjectURL(b);
        const worker = new Worker(url, this.options);
        return worker;
    }
}

and then

import { CorsWorker as Worker } from './cors-worker';

// aliasing CorsWorker to Worker makes it statically analyzable

const corsWorker = await (new Worker(
    new URL('../workers/my-worker.worker', import.meta.url),
    { type: 'module', name: 'my-worker' },
)).createWorker();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants