🐛 fix: resolve desktop upload CORS issue (#11255)

* 🐛 fix: resolve desktop upload CORS issue

Expand CORS bypass to handle all HTTP/HTTPS requests in desktop app.
Previously, CORS bypass only applied to local file server (127.0.0.1),
which caused upload failures when the renderer uses app:// protocol.

Changes:
- Remove Origin header from all requests to prevent CORS preflight
- Add permissive CORS headers to all responses
- Update comments to reflect the new behavior

Resolves LOBE-2581

* 🐛 fix: enhance CORS handling in desktop app

Refine CORS bypass implementation to store and utilize the original Origin header for responses. This change ensures proper CORS headers are added based on the request's origin, improving compatibility with credentialed requests and OPTIONS preflight handling.

Changes:
- Store Origin header for each request and remove it to prevent CORS preflight.
- Add CORS headers to responses using the stored origin.
- Implement caching for OPTIONS requests with a max age.

Resolves LOBE-2581

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix: add onBeforeSendHeaders mock to Browser tests

Enhance the Browser test suite by adding a mock for the onBeforeSendHeaders function in the session's webRequest object. This addition improves the test coverage for CORS handling scenarios.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei
2026-01-05 22:37:43 +08:00
committed by GitHub
parent b887e2125e
commit 49ec5edffb
2 changed files with 49 additions and 20 deletions

View File

@@ -374,14 +374,10 @@ export default class Browser {
| undefined;
logger.info(`Creating new BrowserWindow instance: ${this.identifier}`);
logger.debug(`[${this.identifier}] Options for new window: ${JSON.stringify(this.options)}`);
logger.debug(
`[${this.identifier}] Saved window state: ${JSON.stringify(savedState)}`,
);
logger.debug(`[${this.identifier}] Saved window state: ${JSON.stringify(savedState)}`);
const resolvedState = this.resolveWindowState(savedState, { height, width });
logger.debug(
`[${this.identifier}] Resolved window state: ${JSON.stringify(resolvedState)}`,
);
logger.debug(`[${this.identifier}] Resolved window state: ${JSON.stringify(resolvedState)}`);
const browserWindow = new BrowserWindow({
...res,
@@ -569,33 +565,65 @@ export default class Browser {
}
/**
* Setup CORS bypass for local file server (127.0.0.1:*)
* This is needed for Electron to access files from the local static file server
* Setup CORS bypass for ALL requests
* In production, the renderer uses app://next protocol which triggers CORS for all external requests
* This completely bypasses CORS by:
* 1. Removing Origin header from requests (prevents OPTIONS preflight)
* 2. Adding proper CORS response headers using the stored origin value
*/
private setupCORSBypass(browserWindow: BrowserWindow): void {
logger.debug(`[${this.identifier}] Setting up CORS bypass for local file server`);
logger.debug(`[${this.identifier}] Setting up CORS bypass for all requests`);
const session = browserWindow.webContents.session;
// Intercept response headers to add CORS headers
// Store origin values for each request ID
const originMap = new Map<number, string>();
// Remove Origin header and store it for later use
session.webRequest.onBeforeSendHeaders((details, callback) => {
const requestHeaders = { ...details.requestHeaders };
// Store and remove Origin header to prevent CORS preflight
if (requestHeaders['Origin']) {
originMap.set(details.id, requestHeaders['Origin']);
delete requestHeaders['Origin'];
logger.debug(
`[${this.identifier}] Removed Origin header for: ${details.url} (stored: ${requestHeaders['Origin']})`,
);
}
callback({ requestHeaders });
});
// Add CORS headers to ALL responses using stored origin
session.webRequest.onHeadersReceived((details, callback) => {
const url = details.url;
const responseHeaders = details.responseHeaders || {};
// Only modify headers for local file server requests (127.0.0.1)
if (url.includes('127.0.0.1') || url.includes('lobe-desktop-file')) {
const responseHeaders = details.responseHeaders || {};
// Get the original origin from our map, fallback to default
const origin = originMap.get(details.id) || '*';
// Add CORS headers
responseHeaders['Access-Control-Allow-Origin'] = ['*'];
responseHeaders['Access-Control-Allow-Methods'] = ['GET, POST, PUT, DELETE, OPTIONS'];
responseHeaders['Access-Control-Allow-Headers'] = ['*'];
// Cannot use '*' when Access-Control-Allow-Credentials is true
responseHeaders['Access-Control-Allow-Origin'] = [origin];
responseHeaders['Access-Control-Allow-Methods'] = ['GET, POST, PUT, DELETE, OPTIONS, PATCH'];
responseHeaders['Access-Control-Allow-Headers'] = ['*'];
responseHeaders['Access-Control-Allow-Credentials'] = ['true'];
// Clean up the stored origin after response
originMap.delete(details.id);
// For OPTIONS requests, add preflight cache and override status
if (details.method === 'OPTIONS') {
responseHeaders['Access-Control-Max-Age'] = ['86400']; // 24 hours
logger.debug(`[${this.identifier}] Adding CORS headers to OPTIONS response`);
callback({
responseHeaders,
statusLine: 'HTTP/1.1 200 OK',
});
} else {
callback({ responseHeaders: details.responseHeaders });
return;
}
callback({ responseHeaders });
});
logger.debug(`[${this.identifier}] CORS bypass setup completed`);

View File

@@ -36,6 +36,7 @@ const { mockBrowserWindow, mockNativeTheme, mockIpcMain, mockScreen, MockBrowser
send: vi.fn(),
session: {
webRequest: {
onBeforeSendHeaders: vi.fn(),
onHeadersReceived: vi.fn(),
},
},