Avoiding CORS in Electron: Sending Requests through IPC
I have an Electron app that needs to make several requests to an API. The simplest way to send these requests would be to get your favorite request package installed, turn off web security in Electron’s settings, and start sending requests. However, this is not safe and it can be a hassle to get past all the CORS issues.
Instead, you can use the Node server running your Electron app to send the requests for you. This bypasses CORS because it is a server-to-server request. It doesn’t expose your app to any security vulnerabilities. Best of all it comes ready to use with no config changes and only takes a few lines of code to implement.
Implementation Steps
- create an event listener in
main.js
(the JS file that creates the Electron window i.e. the Electron entry point) - setup request sending and handling in the event listener
- setup an event sender in your frontend JS file
- when you need to send a request send an event with the request data to the event listener
- wait for the promise returned by the event listener to resolve
- handle the response
Creating the Event Listener
An event listener waits for events to be fired from the frontend and returns a promise. This asynchronous behavior is why it is perfect for proxying calls. We can fire an event with the request payload to the event listener and then wait for the promise to be resolved or rejected. If you already have your frontend making API calls switching over to using IPC will not require a major refactor.
In your main.js
file or whatever file you creating the Electron window in:
- import
ipcMain
fromelectron
- import your HTTP library of choice, I’m using
axios
- create the event listener by calling
ipcMain.handle(event_name, listener)
and passing the name of the event and a listener that will be called when events are sent - the listener should be an
async
function so that we canawait
on the call to the API* - call the API and wait for it to return a result
- return the data from the result*
import { app, protocol, BrowserWindow, ipcMain } from ‘electron’
import axios from 'axios'ipcMain.handle('auth', async (event, ...args) => {
console.log('main: auth', event, args) const result = await axios.post(
'https://api.com/auth',
{
username: args[0].username,
password: args[0].password,
auth_type: args[1],
},
) console.log('main: auth result', result)
console.log('main: auth result.data', result.data) return result.data
})
- IPC does not allow you to directly return promises, you can only return basic types and objects that can be serializable. This also means that you can’t return an entire response from the Axios request.
Sending Events
In any frontend JS file:
- import
ipcRenderer
fromelectron
- send an event to the channel using
icpRenderer.invoke(event_name, arguments)
— where are arguments can be any number of arguments that are serializable (you can send objects, arrays, simple types but you can’t send functions or promises) - at this point, we can either wait for the request using
await
or provide it callbacks usingthen
,catch
, orfinally
— I will usethen
to access the data sent back and usecatch
to handle any errors
import { ipcRenderer } from 'electron'
sendAuthRequestUsingIpc() {
return ipcRenderer.invoke('auth',
{
username: AuthService.username,
password: AuthService.password,
},
'password',
).then((data) => { AuthService.AUTH_TOKEN = data['access_token']
return true }).catch((resp) => console.warn(resp))
}
That’s it! Make sure the event name in ipcMain.handle
and ipcRenderer.invoke
are the same.
One Event to Handle all Requests
Now you’re probably thinking that it will be a pain to make an event for each request you want to send, maybe there’s a way to make a generic event to handle all requests? Well, there is!
ipcMain.handle('request', async (_, axios_request) => {
const result = await axios(axios_request)
return { data: result.data, status: result.status }
})
Sending the event:
ipcRenderer.invoke('request',
{
data: {
username: AuthService.username,
password: AuthService.password,
auth_type: 'password',
},
method: 'POST',
url: 'https://api.com/auth',
},
).then((data) => {
AuthService.AUTH_TOKEN = data['access_token']
return true}).catch((resp) => console.warn(resp))
That’s all it takes to use the main process to proxy your CORS calls.
Technical Summary
CORS, Cross-Origin Resource Sharing, is a mechanism with which a server can dictate where its requests can come from. It is also a security restriction implemented by browsers to ensure AJAX requests only communicate with the website you are currently on. Therefore CORS impacts frontend AJAX requests. We can bypass that by sending the AJAX requests using a server instead of the frontend.
Electron has two types of processes, one of which is a server that allows us to send AJAX requests. The main process runs in NodeJS and starts the Chromium process that renders the app. This is the process we will use to make CORS requests because it is a server it won’t run into CORS restrictions. The render process created by the main process renders your app’s frontend and runs the frontend JS. This is a Chromium process which is why there are restrictions on requests you can make using this process.
We can trigger AJAX requests in the main process from the frontend by using IPC. You can use IPC, Inter-Process Communication, to communicate between the main process and its render processes(s). This is accomplished by using ipcMain in the main process and ipcRenderer in the render process. These functions can be used to send events and listen for events. They offer synchronous and asynchronous methods for handling and replying to events. The event payloads have some restrictions (you can’t send functions, promises, etc.) but these restrictions will not impact HTTP requests since those are all simple types.
First, we will create an event listener, channel, in the main process using ipcMain.handle
. The listener will send a POST request using the payload in the event, await
on this request, and then return the response. ipcMain.handle
returns a promise. In the render process, any JS file in the frontend, we will create an event emitter using ipcRenderer.invoke
. We will pass an event name and payload to ipcRenderer.invoke
. Then we will handle the promise returned by ipcMain.handle
with a then
and catch
clause.