Standard Puppeteer gets detected immediately by anti-bot systems. puppeteer-extra-plugin-stealth patches the telltale signs — navigator properties, WebGL, Chrome runtime flags — that reveal automation. Combined with CaptchaAI for solving any CAPTCHA that still appears, you get a powerful stack for reliable browser automation.
This guide covers stealth configuration, detection handling techniques, and CaptchaAI integration for reCAPTCHA, Turnstile, and challenge pages.
Why Stealth Matters for CAPTCHA Automation
Standard Puppeteer exposes ~20 detectable signals:
| Signal | Detection | Stealth Fix |
|---|---|---|
navigator.webdriver |
true in puppeteer |
Set to undefined |
| Chrome runtime | Missing chrome.runtime |
Adds mock object |
| Permissions API | Reveals automation | Patches responses |
| WebGL vendor | Reports SwiftShader | Spoofs real GPU |
| Plugin count | 0 in headless | Injects mock plugins |
| iframe access | Blocked cross-origin | Fixed handling |
Without stealth patches, anti-bot systems serve harder CAPTCHAs or block requests entirely.
Setup
npm install puppeteer-extra puppeteer-extra-plugin-stealth puppeteer
Basic Stealth + CaptchaAI Integration
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
class StealthCaptchaSolver {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseUrl = 'https://ocr.captchaai.com';
}
async launch(options = {}) {
this.browser = await puppeteer.launch({
headless: 'new',
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-blink-features=AutomationControlled',
'--window-size=1920,1080',
],
...options,
});
return this.browser;
}
async solveCaptchaOnPage(page) {
// Auto-detect CAPTCHA type
const captchaInfo = await this.detectCaptcha(page);
if (!captchaInfo) return null;
console.log(`Detected ${captchaInfo.type} CAPTCHA`);
const token = await this.solve(captchaInfo);
// Inject token into page
await this.injectToken(page, captchaInfo.type, token);
return token;
}
async detectCaptcha(page) {
return page.evaluate(() => {
// reCAPTCHA v2
const recaptchaV2 = document.querySelector('[data-sitekey]');
if (recaptchaV2) {
return {
type: 'recaptcha_v2',
sitekey: recaptchaV2.getAttribute('data-sitekey'),
pageUrl: window.location.href,
};
}
// reCAPTCHA v3 (in script)
const scripts = document.querySelectorAll('script[src*="recaptcha"]');
for (const script of scripts) {
const src = script.src;
const renderMatch = src.match(/render=([A-Za-z0-9_-]+)/);
if (renderMatch && renderMatch[1] !== 'explicit') {
return {
type: 'recaptcha_v3',
sitekey: renderMatch[1],
pageUrl: window.location.href,
};
}
}
// Turnstile
const turnstile = document.querySelector('.cf-turnstile[data-sitekey]');
if (turnstile) {
return {
type: 'turnstile',
sitekey: turnstile.getAttribute('data-sitekey'),
pageUrl: window.location.href,
};
}
return null;
});
}
async solve(captchaInfo) {
const params = { key: this.apiKey, json: 1 };
switch (captchaInfo.type) {
case 'recaptcha_v2':
params.method = 'userrecaptcha';
params.googlekey = captchaInfo.sitekey;
params.pageurl = captchaInfo.pageUrl;
break;
case 'recaptcha_v3':
params.method = 'userrecaptcha';
params.googlekey = captchaInfo.sitekey;
params.pageurl = captchaInfo.pageUrl;
params.version = 'v3';
params.action = 'verify';
params.min_score = '0.7';
break;
case 'turnstile':
params.method = 'turnstile';
params.key = captchaInfo.sitekey;
params.pageurl = captchaInfo.pageUrl;
break;
}
// Submit
const submitResp = await fetch(`${this.baseUrl}/in.php`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(params),
});
const submitData = await submitResp.json();
if (submitData.status !== 1) throw new Error(`Submit: ${submitData.request}`);
// Poll
const taskId = submitData.request;
for (let i = 0; i < 60; i++) {
await new Promise(r => setTimeout(r, 5000));
const pollResp = await fetch(
`${this.baseUrl}/res.php?key=${this.apiKey}&action=get&id=${taskId}&json=1`
);
const pollData = await pollResp.json();
if (pollData.request === 'CAPCHA_NOT_READY') continue;
if (pollData.status !== 1) throw new Error(`Solve: ${pollData.request}`);
return pollData.request;
}
throw new Error('Timeout');
}
async injectToken(page, type, token) {
switch (type) {
case 'recaptcha_v2':
case 'recaptcha_v3':
await page.evaluate((t) => {
document.querySelector('#g-recaptcha-response').value = t;
// Also set in iframe textarea if present
const iframes = document.querySelectorAll('iframe[src*="recaptcha"]');
iframes.forEach(iframe => {
try {
const textarea = iframe.contentDocument?.querySelector('#g-recaptcha-response');
if (textarea) textarea.value = t;
} catch (e) {}
});
// Trigger callback
if (typeof window.___grecaptcha_cfg !== 'undefined') {
const clients = Object.values(window.___grecaptcha_cfg.clients || {});
for (const client of clients) {
const callback = Object.values(client).find(
v => typeof v === 'object' && v?.callback
);
if (callback?.callback) callback.callback(t);
}
}
}, token);
break;
case 'turnstile':
await page.evaluate((t) => {
const input = document.querySelector('[name="cf-turnstile-response"]');
if (input) input.value = t;
// Trigger turnstile callback
if (window.turnstile) {
const widgets = document.querySelectorAll('.cf-turnstile');
widgets.forEach(w => {
const callback = w.getAttribute('data-callback');
if (callback && window[callback]) window[callback](t);
});
}
}, token);
break;
}
}
async close() {
if (this.browser) await this.browser.close();
}
}
Complete Workflow Example
async function automateProtectedSite() {
const solver = new StealthCaptchaSolver('YOUR_API_KEY');
try {
await solver.launch();
const page = await solver.browser.newPage();
// Set realistic viewport and user agent
await page.setViewport({ width: 1920, height: 1080 });
// Navigate to target
await page.goto('https://example.com/login', { waitUntil: 'networkidle2' });
// Fill form fields
await page.type('#email', 'user@example.com', { delay: 50 });
await page.type('#password', 'password123', { delay: 50 });
// Solve CAPTCHA
const token = await solver.solveCaptchaOnPage(page);
console.log(`CAPTCHA solved: ${token?.substring(0, 50)}...`);
// Submit form
await page.click('#login-button');
await page.waitForNavigation({ waitUntil: 'networkidle2' });
console.log(`Current URL: ${page.url()}`);
} finally {
await solver.close();
}
}
automateProtectedSite().catch(console.error);
Advanced Stealth Configuration
Selective Plugin Evasions
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
const stealth = StealthPlugin();
// Enable/disable specific evasions
stealth.enabledEvasions.delete('chrome.runtime');
stealth.enabledEvasions.add('navigator.permissions');
puppeteer.use(stealth);
Custom Browser Fingerprint
async function setupFingerprint(page) {
// Override WebGL renderer
await page.evaluateOnNewDocument(() => {
const getParameter = WebGLRenderingContext.prototype.getParameter;
WebGLRenderingContext.prototype.getParameter = function(parameter) {
if (parameter === 37445) return 'Intel Inc.';
if (parameter === 37446) return 'Intel Iris OpenGL Engine';
return getParameter.call(this, parameter);
};
});
// Set realistic timezone
await page.emulateTimezone('America/New_York');
// Set navigator properties
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 8 });
Object.defineProperty(navigator, 'deviceMemory', { get: () => 8 });
Object.defineProperty(navigator, 'platform', { get: () => 'Win32' });
});
}
Handling Cloudflare Challenge Pages
async function handleCloudflareChallenge(page) {
// Wait for challenge to appear
const isChallenged = await page.evaluate(() => {
return document.title.includes('Just a moment') ||
document.querySelector('#challenge-running') !== null;
});
if (!isChallenged) return;
console.log('Cloudflare challenge detected');
// Check for Turnstile widget
await page.waitForSelector('.cf-turnstile', { timeout: 10000 }).catch(() => null);
const turnstileKey = await page.evaluate(() => {
const el = document.querySelector('.cf-turnstile[data-sitekey]');
return el?.getAttribute('data-sitekey');
});
if (turnstileKey) {
const solver = new StealthCaptchaSolver('YOUR_API_KEY');
const token = await solver.solve({
type: 'turnstile',
sitekey: turnstileKey,
pageUrl: page.url(),
});
await page.evaluate((t) => {
const input = document.querySelector('[name="cf-turnstile-response"]');
if (input) {
input.value = t;
input.dispatchEvent(new Event('change', { bubbles: true }));
}
}, token);
await page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 30000 });
}
}
Request Interception for CAPTCHA Detection
async function interceptCaptchaRequests(page) {
const captchaRequests = [];
await page.setRequestInterception(true);
page.on('request', (request) => {
const url = request.url();
// Log CAPTCHA-related requests
if (url.includes('recaptcha') || url.includes('turnstile') || url.includes('hcaptcha')) {
captchaRequests.push({
url,
type: url.includes('recaptcha') ? 'recaptcha' :
url.includes('turnstile') ? 'turnstile' : 'hcaptcha',
timestamp: Date.now(),
});
}
// Block unnecessary resources for speed
const blocked = ['image', 'stylesheet', 'font'];
if (blocked.includes(request.resourceType()) && !url.includes('captcha')) {
request.abort();
} else {
request.continue();
}
});
return captchaRequests;
}
Multi-Page Session Management
async function multiPageWorkflow(apiKey) {
const solver = new StealthCaptchaSolver(apiKey);
await solver.launch();
const page = await solver.browser.newPage();
// Step 1: Login page
await page.goto('https://example.com/login', { waitUntil: 'networkidle2' });
await page.type('#email', 'user@example.com', { delay: 30 });
await page.type('#password', 'password', { delay: 30 });
await solver.solveCaptchaOnPage(page);
await page.click('#submit');
await page.waitForNavigation();
// Step 2: Navigate to protected area
await page.goto('https://example.com/dashboard', { waitUntil: 'networkidle2' });
// Step 3: Trigger another CAPTCHA
await page.click('#export-data');
await page.waitForSelector('[data-sitekey]', { timeout: 5000 }).catch(() => null);
await solver.solveCaptchaOnPage(page);
// Collect cookies for future requests
const cookies = await page.cookies();
console.log(`Session cookies: ${cookies.length}`);
await solver.close();
return cookies;
}
Detection Testing
Verify your stealth setup works:
async function testStealth() {
const solver = new StealthCaptchaSolver('YOUR_API_KEY');
await solver.launch();
const page = await solver.browser.newPage();
// Test against popular detection sites
await page.goto('https://bot.sannysoft.com/');
await page.screenshot({ path: 'stealth-test.png', fullPage: true });
// Check specific values
const results = await page.evaluate(() => ({
webdriver: navigator.webdriver,
languages: navigator.languages,
plugins: navigator.plugins.length,
platform: navigator.platform,
hardwareConcurrency: navigator.hardwareConcurrency,
}));
console.log('Detection results:', results);
await solver.close();
}
Troubleshooting
| Issue | Cause | Fix |
|---|---|---|
| Still detected as bot | Missing stealth evasions | Update puppeteer-extra-plugin-stealth to latest |
| CAPTCHA not found | Dynamic loading | Add waitForSelector before detection |
| Token injection fails | Shadow DOM or iframe | Use page.frames() to access CAPTCHA frame |
| Browser crashes | Memory leak | Close pages after use, limit concurrent tabs |
| Slow performance | Blocking resources | Use request interception to block images/CSS |
ERR_CERT_AUTHORITY_INVALID |
Self-signed cert | Add --ignore-certificate-errors arg |
FAQ
Does stealth mode guarantee no CAPTCHAs?
No. Stealth reduces the chance of being served CAPTCHAs, but some sites always show them. CaptchaAI handles the ones that appear.
Should I use headless or headed mode?
Headless 'new' mode with stealth plugin passes most detection. Use headed mode only for debugging.
How often should I rotate user agents?
Rotate per session, not per page. Changing user agent mid-session is a detection signal itself.
Can I use this with Puppeteer Cluster?
Yes. puppeteer-extra works with puppeteer-cluster for parallel browser instances.
Related Guides
- Puppeteer + CaptchaAI Advanced Patterns
- Browser Fingerprinting and CAPTCHA Guide
- Headless vs Headed Chrome for CAPTCHA
Build stealth-optimized browser automation — get your CaptchaAI API key and integrate with Puppeteer Stealth.
Discussions (0)
Join the conversation
Sign in to share your opinion.
Sign InNo comments yet.