Skip to content

fix: handling unexpected browser context closed error #498

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

Merged
merged 1 commit into from
Mar 27, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
243 changes: 144 additions & 99 deletions server/src/browser-management/classes/RemoteBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,105 +245,150 @@ export class RemoteBrowser {
* @returns {Promise<void>}
*/
public initialize = async (userId: string): Promise<void> => {
this.browser = <Browser>(await chromium.launch({
headless: true,
args: [
"--disable-blink-features=AutomationControlled",
"--disable-web-security",
"--disable-features=IsolateOrigins,site-per-process",
"--disable-site-isolation-trials",
"--disable-extensions",
"--no-sandbox",
"--disable-dev-shm-usage",
"--force-color-profile=srgb",
"--force-device-scale-factor=2",
],
}));
const proxyConfig = await getDecryptedProxyConfig(userId);
let proxyOptions: { server: string, username?: string, password?: string } = { server: '' };
if (proxyConfig.proxy_url) {
proxyOptions = {
server: proxyConfig.proxy_url,
...(proxyConfig.proxy_username && proxyConfig.proxy_password && {
username: proxyConfig.proxy_username,
password: proxyConfig.proxy_password,
}),
};
}
const contextOptions: any = {
// viewport: { height: 400, width: 900 },
// recordVideo: { dir: 'videos/' }
// Force reduced motion to prevent animation issues
reducedMotion: 'reduce',
// Force JavaScript to be enabled
javaScriptEnabled: true,
// Set a reasonable timeout
timeout: 50000,
// Disable hardware acceleration
forcedColors: 'none',
isMobile: false,
hasTouch: false,
userAgent: this.getUserAgent(),
deviceScaleFactor: 2,
};

if (proxyOptions.server) {
contextOptions.proxy = {
server: proxyOptions.server,
username: proxyOptions.username ? proxyOptions.username : undefined,
password: proxyOptions.password ? proxyOptions.password : undefined,
};
}

this.context = await this.browser.newContext(contextOptions);
await this.context.addInitScript(
`const defaultGetter = Object.getOwnPropertyDescriptor(
Navigator.prototype,
"webdriver"
).get;
defaultGetter.apply(navigator);
defaultGetter.toString();
Object.defineProperty(Navigator.prototype, "webdriver", {
set: undefined,
enumerable: true,
configurable: true,
get: new Proxy(defaultGetter, {
apply: (target, thisArg, args) => {
Reflect.apply(target, thisArg, args);
return false;
},
}),
});
const patchedGetter = Object.getOwnPropertyDescriptor(
Navigator.prototype,
"webdriver"
).get;
patchedGetter.apply(navigator);
patchedGetter.toString();`
);
this.currentPage = await this.context.newPage();

await this.setupPageEventListeners(this.currentPage);

const viewportSize = await this.currentPage.viewportSize();
if (viewportSize) {
this.socket.emit('viewportInfo', {
width: viewportSize.width,
height: viewportSize.height,
userId: this.userId
});
}

try {
const blocker = await PlaywrightBlocker.fromLists(fetch, ['https://easylist.to/easylist/easylist.txt']);
await blocker.enableBlockingInPage(this.currentPage);
this.client = await this.currentPage.context().newCDPSession(this.currentPage);
await blocker.disableBlockingInPage(this.currentPage);
console.log('Adblocker initialized');
} catch (error: any) {
console.warn('Failed to initialize adblocker, continuing without it:', error.message);
// Still need to set up the CDP session even if blocker fails
this.client = await this.currentPage.context().newCDPSession(this.currentPage);
const MAX_RETRIES = 3;
let retryCount = 0;
let success = false;

while (!success && retryCount < MAX_RETRIES) {
try {
this.browser = <Browser>(await chromium.launch({
headless: true,
args: [
"--disable-blink-features=AutomationControlled",
"--disable-web-security",
"--disable-features=IsolateOrigins,site-per-process",
"--disable-site-isolation-trials",
"--disable-extensions",
"--no-sandbox",
"--disable-dev-shm-usage",
"--force-color-profile=srgb",
"--force-device-scale-factor=2",
],
}));

if (!this.browser || this.browser.isConnected() === false) {
throw new Error('Browser failed to launch or is not connected');
}

const proxyConfig = await getDecryptedProxyConfig(userId);
let proxyOptions: { server: string, username?: string, password?: string } = { server: '' };

if (proxyConfig.proxy_url) {
proxyOptions = {
server: proxyConfig.proxy_url,
...(proxyConfig.proxy_username && proxyConfig.proxy_password && {
username: proxyConfig.proxy_username,
password: proxyConfig.proxy_password,
}),
};
}

const contextOptions: any = {
// viewport: { height: 400, width: 900 },
// recordVideo: { dir: 'videos/' }
// Force reduced motion to prevent animation issues
reducedMotion: 'reduce',
// Force JavaScript to be enabled
javaScriptEnabled: true,
// Set a reasonable timeout
timeout: 50000,
// Disable hardware acceleration
forcedColors: 'none',
isMobile: false,
hasTouch: false,
userAgent: this.getUserAgent(),
deviceScaleFactor: 2,
};

if (proxyOptions.server) {
contextOptions.proxy = {
server: proxyOptions.server,
username: proxyOptions.username ? proxyOptions.username : undefined,
password: proxyOptions.password ? proxyOptions.password : undefined,
};
}

await new Promise(resolve => setTimeout(resolve, 500));

const contextPromise = this.browser.newContext(contextOptions);
this.context = await Promise.race([
contextPromise,
new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('Context creation timed out after 15s')), 15000);
})
]) as BrowserContext;

await this.context.addInitScript(
`const defaultGetter = Object.getOwnPropertyDescriptor(
Navigator.prototype,
"webdriver"
).get;
defaultGetter.apply(navigator);
defaultGetter.toString();
Object.defineProperty(Navigator.prototype, "webdriver", {
set: undefined,
enumerable: true,
configurable: true,
get: new Proxy(defaultGetter, {
apply: (target, thisArg, args) => {
Reflect.apply(target, thisArg, args);
return false;
},
}),
});
const patchedGetter = Object.getOwnPropertyDescriptor(
Navigator.prototype,
"webdriver"
).get;
patchedGetter.apply(navigator);
patchedGetter.toString();`
);

this.currentPage = await this.context.newPage();
await this.setupPageEventListeners(this.currentPage);

const viewportSize = await this.currentPage.viewportSize();
if (viewportSize) {
this.socket.emit('viewportInfo', {
width: viewportSize.width,
height: viewportSize.height,
userId: this.userId
});
}

try {
const blocker = await PlaywrightBlocker.fromLists(fetch, ['https://easylist.to/easylist/easylist.txt']);
await blocker.enableBlockingInPage(this.currentPage);
this.client = await this.currentPage.context().newCDPSession(this.currentPage);
await blocker.disableBlockingInPage(this.currentPage);
console.log('Adblocker initialized');
} catch (error: any) {
console.warn('Failed to initialize adblocker, continuing without it:', error.message);
// Still need to set up the CDP session even if blocker fails
this.client = await this.currentPage.context().newCDPSession(this.currentPage);
}

success = true;
logger.log('debug', `Browser initialized successfully for user ${userId}`);
} catch (error: any) {
retryCount++;
logger.log('error', `Browser initialization failed (attempt ${retryCount}/${MAX_RETRIES}): ${error.message}`);

if (this.browser) {
try {
await this.browser.close();
} catch (closeError) {
logger.log('warn', `Failed to close browser during cleanup: ${closeError}`);
}
this.browser = null;
}

if (retryCount >= MAX_RETRIES) {
throw new Error(`Failed to initialize browser after ${MAX_RETRIES} attempts: ${error.message}`);
}

await new Promise(resolve => setTimeout(resolve, 1000));
}
Comment on lines +374 to +391
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve cleanup logic in the catch block.
When cleaning up after a failed attempt, ensure that partial initializations (e.g., a context that started to initialize) are also explicitly closed if needed. Currently, only the browser is gracefully closed. If a browser context was partially created and left behind, it might lead to resource leaks.

}
};

Expand Down