electron: IpcRenderer does not execute normally in preload.js

Preflight Checklist

  • I have read the Contributing Guidelines for this project.
  • I agree to follow the Code of Conduct that this project adheres to.
  • I have searched the issue tracker for an issue that matches the one I want to file, without success.

Issue Details

  • Electron Version:
    • 7.1.3
  • Operating System:
    • Windows 10 (1909)
  • Last Known Working Electron version:
    • never

Expected Behavior

In preload.js, ipcRenderer.on can work when script loading.

Actual Behavior

In preload.js, ipcRenderer.on does not work while script loaded.

To Reproduce

https://gist.github.com/8beadb34f2123ee942968494edf439d0

Screenshots

image

image

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Comments: 28 (7 by maintainers)

Most upvoted comments

@reZach That is not a solution and is a terrible idea. The problem is not the context bridge.

Copying your desired state from your other issue, this is how you do it correctly:

renderer

class LibraryClass {
    constructor(myValue = 1) {
        this.classValue = myValue;

        window.electron.ipcRenderer.on("response", (IpcRendererEvent, args) => {
            if (args.success) this.classValue++;
        });
    }

    send() {
        window.electron.ipcRenderer.send("request", {
            data: this.classValue
        });
    }
}

main

let win = new BrowserWindow({...});

ipcMain.on("request", (IpcMainEvent, args) => {
    // Use node module (ie. "fs");
    // perhaps save value of args.data to file with a timestamp

    win.webContents.send("response", {
        success: true
    });
});

Isolated Preload

const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('myapi', {
  request: (data) => ipcRenderer.send("request", {
    data,
  }),
  onResponse: (fn) => {
    // Deliberately strip event as it includes `sender` 
    ipcRenderer.on('response', (event, ...args) => fn(...args));
  }
}

Renderer

class LibraryClass {
    constructor(myValue = 1) {
        this.classValue = myValue;

        window.myapi.onResponse((args) => {
            if (args.success) this.classValue++;
        });
    }

    send() {
        window.myapi.request(this.classValue);
    }
}

Main Process

let win = new BrowserWindow({...});

ipcMain.on("request", (IpcMainEvent, args) => {
    // Use node module (ie. "fs");
    // perhaps save value of args.data to file with a timestamp

    win.webContents.send("response", {
        success: true
    });
});

Things to note

  1. Exposing a minimal API surface from the preload to the renderer, just two helpers, not the entire ipcRenderer module
  2. Core business logic still remains in the renderer (LibraryClass) just the stuff that requires Electron APIs lives in the isolated preload
  3. Even with this set up you should validate the incoming arguments are valid / what you would expect when you are handling the event in the main process

Final Note

@reZach I’d ask that you stop spamming these issues as your issue is not this potential bug rather user issues with how the APIs work.

Here is another simple way to avoid the problem of not being able to remove the listener because of that function references issues:

// preload.js
contextBridge.exposeInMainWorld('ipc', {
  on: (channel, listener) => {
    ipcRenderer.on(channel, listener);
    return () => {
      ipcRenderer.removeListener(channel, listener);
    };
  },
});

// renderer process
const removeListener = window.ipc.on('channel', () => {
  // listener code
});
removeListener();

@Slapbox Unfortunately that wouldn’t work as every time a function is passed over the bridge a new proxy function is made. i.e.

function A (world 1) --> function B (world 2) function B (world 2) --> function C (world 1)

It’s a known inefficiency of the contextBridge.

How I’d do this is by using some kind of token system.

const listeners = {};

contextBridge.exposeInMainWorld('myapi', {
  request: (data) => ipcRenderer.send("request", {
    data,
  }),
  onResponse: (fn) => {
    const saferFn = (event, ...args) => fn(...args)
    // Deliberately strip event as it includes `sender` 
    ipcRenderer.on('response', saferFn);
    const key = symbol();
    listeners[key] = saferFn;
    return key;
  },
  removeResponseHandler: (key) => {
    const fn = listeners[key];
    delete listeners[key];
    ipcRenderer.removeListener('response', fn);
  }
}

You can always make a pretty helper for this to make the usage for multiple IPC events much cleaner

@MarshallOfSound Yes, this is my fault. I continued the conversation after I had realized the issue was not related to @ChinaLiuxiaosong’s. I did so to help anyone who might find value in the process to work around my specific issue, not the one created.

Thank you for steering me on the right course for this repo with you are a member of. I will remember my faults and not repeat them again.

I’ve posted a $230 bounty on this issue for prioritization, thanks!

@reZach That is not a solution and is a terrible idea. The problem is not the context bridge.

Copying your desired state from your other issue, this is how you do it correctly:

Isolated Preload

const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('myapi', {
  request: (data) => ipcRenderer.send("request", {
    data,
  }),
  onResponse: (fn) => {
    // Deliberately strip event as it includes `sender` 
    ipcRenderer.on('response', (event, ...args) => fn(...args));
  }
}

Renderer

class LibraryClass {
    constructor(myValue = 1) {
        this.classValue = myValue;

        window.myapi.onResponse((args) => {
            if (args.success) this.classValue++;
        });
    }

    send() {
        window.myapi.request(this.classValue);
    }
}

Main Process

let win = new BrowserWindow({...});

ipcMain.on("request", (IpcMainEvent, args) => {
    // Use node module (ie. "fs");
    // perhaps save value of args.data to file with a timestamp

    win.webContents.send("response", {
        success: true
    });
});

Things to note

  1. Exposing a minimal API surface from the preload to the renderer, just two helpers, not the entire ipcRenderer module
  2. Core business logic still remains in the renderer (LibraryClass) just the stuff that requires Electron APIs lives in the isolated preload
  3. Even with this set up you should validate the incoming arguments are valid / what you would expect when you are handling the event in the main process

Final Note

@reZach I’d ask that you stop spamming these issues as your issue is not this potential bug rather user issues with how the APIs work.

this worked! thank you.

@reZach That is not a solution and is a terrible idea. The problem is not the context bridge.

Copying your desired state from your other issue, this is how you do it correctly:

Isolated Preload

const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('myapi', {
  request: (data) => ipcRenderer.send("request", {
    data,
  }),
  onResponse: (fn) => {
    // Deliberately strip event as it includes `sender` 
    ipcRenderer.on('response', (event, ...args) => fn(...args));
  }
}

Renderer

class LibraryClass {
    constructor(myValue = 1) {
        this.classValue = myValue;

        window.myapi.onResponse((args) => {
            if (args.success) this.classValue++;
        });
    }

    send() {
        window.myapi.request(this.classValue);
    }
}

Main Process

let win = new BrowserWindow({...});

ipcMain.on("request", (IpcMainEvent, args) => {
    // Use node module (ie. "fs");
    // perhaps save value of args.data to file with a timestamp

    win.webContents.send("response", {
        success: true
    });
});

Things to note

  1. Exposing a minimal API surface from the preload to the renderer, just two helpers, not the entire ipcRenderer module
  2. Core business logic still remains in the renderer (LibraryClass) just the stuff that requires Electron APIs lives in the isolated preload
  3. Even with this set up you should validate the incoming arguments are valid / what you would expect when you are handling the event in the main process

Final Note

@reZach I’d ask that you stop spamming these issues as your issue is not this potential bug rather user issues with how the APIs work.

Thanks for sharing this @MarshallOfSound. I just found it while looking into a similar issue. This seems to solve my problem but, now I’m wondering, wouldn’t sending a callback to the preload script potentially give access to Electron and node APIs to a hijacker?

Also, if I’m not mistaken there’s a memory leak here right?:

onResponse: (fn) => {
     // Deliberately strip event as it includes `sender` 
     ipcRenderer.on('response', (event, ...args) => fn(...args));
}

The event handler is being created over and over and never cleaned up.

Easy to expose Electron APIs (ipcRenderer,webFrame,process) to renderer with @electron-toolkit/preload

Closing due to lack of activity

I would gladly help if someone could point me in the right direction, my familiarity with the source code is very low.