Perché Puppeteer Nudo Viene Bloccato Subito
Se hai mai lanciato uno script Puppeteer contro un sito protetto da Cloudflare, Datadome o PerimeterX, sai già cosa succede: una pagina di challenge, un blocco silenzioso, o un HTTP 403 immediato. Il problema non è il tuo codice — è che il browser automatizzato trasmette decine di segnali che lo identificano come non-umano.
I sistemi anti-bot moderni non si limitano a controllare un singolo header. Costruiscono un fingerprint composito del visitatore: proprietà del navigator, comportamento degli oggetti JavaScript, pattern di rete, e coerenza tra tutti questi elementi. Un browser controllato da Puppeteer fallisce su molteplici fronti simultaneamente.
I segnali di rilevamento più comuni
- navigator.webdriver = true — La W3C WebDriver spec richiede che questa proprietà sia
truequando il browser è sotto automazione. Puppeteer non la nasconde. - plugins array vuoto o inconsistente — I browser reali riportano plugin come Chrome PDF Viewer; Chromium headless spesso riporta un array vuoto.
- iframe chrome.runtime — L'artefatto
chrome.runtimesu iframes è presente nelle estensioni Chromium ma assente o inconsistente in Puppeteer. - Canvas e WebGL fingerprint identiche — Ogni istanza headless produce lo stesso hash canvas/WebGL, creando cluster di fingerprint identiche che i sistemi anti-bot rilevano come bot farm.
- User-Agent incoerente — Puppeteer usa un UA generico che spesso non corrisponde alla piattaforma reale (es. UA Windows con
navigator.platform = MacIntel). - Missing WebRTC IPs — I browser reali espongono IP locali tramite WebRTC; Chromium headless con
--disable-webrtcnon lo fa.
Qualsiasi singolo segnale può bastare per un blocco. I sistemi avanzati li combinano in un confidence score: anche se superi il test navigator.webdriver, una canvas fingerprint identica a migliaia di altre sessioni ti banna comunque.
Puppeteer-Extra e il Plugin Stealth: Cosa Patcha Realmente
puppeteer-extra è un wrapper modulare per Puppeteer che supporta un sistema di plugin. Il più critico per l'anti-rilevamento è puppeteer-extra-plugin-stealth, che applica patch a 10+ superfici di rilevamento attraverso un sistema di Page.evaluateOnNewDocument hooks.
Ecco i principali sottoplugin stealth e cosa fanno:
| Stealth Sub-plugin | Segnale Patchato | Tecnica |
|---|---|---|
| webdriver | navigator.webdriver = true | Ridefinisce getter per restituire undefined |
| chrome.runtime | iframe chrome artifacts | Inietta mock di chrome.runtime nei frames |
| plugin.length | plugins array vuoto | Aggiunge plugin fittizi realistici |
| languages | navigator.languages inconsistente | Override con lingue coerenti col UA |
| iframe.contentWindow | cross-origin iframe detection | Normalizza proprietà cross-origin |
| webgl.vendor | WebGL vendor/renderer sospetti | Override di vendor e renderer strings |
| media.codecs | supporta codecs mancanti | Riempie il mock di codecs supportati |
| user-agent-override | UA non coerente con platform | Sincronizza UA, platform, appVersion |
Il plugin stealth agisce come un middleware di inizializzazione: prima che qualsiasi JavaScript della pagina venga eseguito, inietta gli override tramite evaluateOnNewDocument. Questo è fondamentale — se le patch arrivano dopo il primo script della pagina, il rilevamento è già avvenuto.
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
// Registra il plugin stealth come middleware
puppeteer.use(StealthPlugin());
(async () => {
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
await page.goto('https://bot.sannysoft.com/');
// Verifica: navigator.webdriver deve essere undefined
const isUndetected = await page.evaluate(() => {
return navigator.webdriver === undefined;
});
console.log('Stealth attivo:', isUndetected); // true
await browser.close();
})();Questo risolve la maggior parte dei controlli JavaScript lato client. Ma c'è un problema: il plugin stealth non tocca il livello di rete. Il tuo IP datacenter, la mancanza di cookie storici, e il pattern di richiesta restano quelli di un bot.
Combinare Stealth con Proxy Residenziali: Lo Stack Anti-Rilevamento Completo
Il vero vantaggio competitivo arriva quando combini patch lato browser (stealth) con patch lato rete (proxy residenziali). Un sito anti-bot vede: un browser con fingerprint coerente e un IP residenziale con storico di traffico legittimo. Questo è il gold standard per il Puppeteer anti-detection.
I proxy residenziali sono cruciali perché:
- Gli IP datacenter sono catalogati in range AS dedicati — i sistemi anti-bot mantengono database di ASN datacenter e li bloccano proattivamente.
- Gli IP residenziali provengono da ISP veri, con reputazione storica e pattern di traffico realistici.
- La rotazione per-request garantisce che ogni richiesta parte da un contesto IP fresco, rendendo impossibile il rate-limiting basato su IP.
Con ProxyHat, la configurazione è diretta:
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
// Configurazione proxy residenziale ProxyHat
const PROXY_HOST = 'gate.proxyhat.com';
const PROXY_PORT = 8080;
const PROXY_USER = 'your-username-country-US';
const PROXY_PASS = 'your-password';
(async () => {
const browser = await puppeteer.launch({
headless: 'new',
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
`--proxy-server=http://${PROXY_HOST}:${PROXY_PORT}`
]
});
const page = await browser.newPage();
// Autenticazione proxy
await page.authenticate({
username: PROXY_USER,
password: PROXY_PASS
});
await page.goto('https://httpbin.org/ip');
const ip = await page.evaluate(() =>
document.querySelector('pre').innerText
);
console.log('IP residenziale:', ip);
await browser.close();
})();Nota il pattern country-US nel username — ProxyHat supporta geo-targeting a livello di nazione e città, essenziale per lo SERP tracking localizzato.
Perché non basta un solo layer
Un errore comune: usare proxy residenziali senza stealth, o stealth senza proxy residenziali. Ecco perché entrambi sono necessari:
| Stack | Rilevamento JS | Rilevamento IP/ASN | Success Rate Stimato |
|---|---|---|---|
| Puppeteer nudo | ❌ Fail immediato | ❌ IP datacenter bloccato | 5-15% |
| + Stealth plugin | ✅ Patch applicate | ❌ IP datacenter bloccato | 30-50% |
| + Proxy residenziali | ❌ Fail immediato | ✅ IP residenziale pulito | 20-40% |
| Stealth + Proxy residenziali | ✅ Patch applicate | ✅ IP residenziale pulito | 85-95% |
I numeri parlano chiaro: il layer JavaScript e il layer di rete coprono superfici di attacco diverse. Entrambi sono necessari per la produzione.
Custom Evaluators: Randomizzazione Canvas e WebGL Per-Session
Il plugin stealth standardizza le fingerprint — ma se 1000 istanze usano la stessa fingerprint standardizzata, i sistemi anti-bot rilevano il cluster. La soluzione: randomizzare le fingerprint per sessione aggiungendo rumore controllato a canvas e WebGL.
Questo è il punto dove puppeteer-extra brilla davvero: puoi scrivere plugin custom che si integrano nello stesso middleware pipeline del plugin stealth.
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
// Plugin custom per canvas/WebGL fingerprint randomization
class FingerprintRandomizerPlugin extends puppeteer.Plugin {
constructor(opts = {}) {
super(opts);
this.noiseLevel = opts.noiseLevel || 0.01;
}
get name() { return 'fingerprint-randomizer'; }
async onPageCreated(page) {
const seed = Math.random() * 10000;
const noise = this.noiseLevel;
await page.evaluateOnNewDocument((seed, noise) => {
// Canvas noise: aggiunge micro-variazioni ai pixel
const origToDataURL = HTMLCanvasElement.prototype.toDataURL;
HTMLCanvasElement.prototype.toDataURL = function(...args) {
const ctx = this.getContext('2d');
if (ctx) {
const imgData = ctx.getImageData(0, 0, this.width, this.height);
for (let i = 0; i < imgData.data.length; i += 4) {
imgData.data[i] += Math.round(
(Math.sin(seed + i) * noise) * 2
);
}
ctx.putImageData(imgData, 0, 0);
}
return origToDataURL.apply(this, args);
};
// WebGL vendor/renderer randomization
const getParameterOrig = WebGLRenderingContext.prototype.getParameter;
const vendors = ['Google Inc.', 'Google Inc. (NVIDIA)', 'Google Inc. (Intel)'];
const renderers = [
'ANGLE (NVIDIA GeForce GTX 1060)',
'ANGLE (Intel HD Graphics 630)',
'ANGLE (AMD Radeon RX 580)'
];
WebGLRenderingContext.prototype.getParameter = function(param) {
if (param === 0x1F00) return vendors[Math.floor(seed) % vendors.length]; // UNMASKED_VENDOR
if (param === 0x1F01) return renderers[Math.floor(seed) % renderers.length]; // UNMASKED_RENDERER
return getParameterOrig.call(this, param);
};
}, seed, noise);
}
}
puppeteer.use(StealthPlugin());
puppeteer.use(new FingerprintRandomizerPlugin({ noiseLevel: 0.02 }));Il pattern Math.sin(seed + i) è importante: produce variazioni deterministiche per sessione ma apparentemente casuali tra sessioni diverse. Questo significa che una singola sessione ha una fingerprint coerente (necessario per non innescare controlli di coerenza intra-sessione), ma sessioni diverse hanno fingerprint diverse (necessario per non formare cluster rilevabili).
Rotazione Proxy Per-Browser-Context
Puppeteer supporta BrowserContext (simile ai profili di Firefox): contesti isolati con cookie, storage e cache separati. Questo è il livello giusto per la rotazione dei proxy — ogni context riceve il proprio IP residenziale e la propria fingerprint.
Il modo idiomatico per implementare la rotazione è attraverso un middleware proxy manager che assegna un proxy diverso ad ogni context:
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
const GATE = 'gate.proxyhat.com';
const PORT = 8080;
const USER = 'your-username';
const PASS = 'your-password';
// Genera credenziali proxy con geo-targeting e sessione sticky
class ProxyRotator {
constructor(countries = ['US', 'DE', 'GB', 'FR', 'JP']) {
this.countries = countries;
this.counter = 0;
}
next() {
const country = this.countries[
this.counter % this.countries.length
];
const sessionId = `sess-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
this.counter++;
return {
server: `http://${GATE}:${PORT}`,
username: `${USER}-country-${country}-session-${sessionId}`,
password: PASS,
country
};
}
}
// Crea un BrowserContext isolato con proxy dedicato
async function createContextWithProxy(browser, proxyRotator) {
const proxy = proxyRotator.next();
const context = await browser.createBrowserContext({
proxyServer: proxy.server
});
const page = await context.newPage();
await page.authenticate({
username: proxy.username,
password: proxy.password
});
return { context, page, proxy };
}
(async () => {
const browser = await puppeteer.launch({ headless: 'new' });
const rotator = new ProxyRotator(['US', 'DE', 'GB']);
// Esegui 3 task con IP e contesti diversi
const tasks = [
'https://httpbin.org/ip',
'https://httpbin.org/headers',
'https://httpbin.org/ip'
];
for (const url of tasks) {
const { context, page, proxy } = await createContextWithProxy(browser, rotator);
console.log(`Context con IP ${proxy.country}:`);
await page.goto(url, { waitUntil: 'networkidle2' });
const content = await page.evaluate(() => document.body.innerText);
console.log(content);
await context.close(); // Chiude context + libera IP
}
await browser.close();
})();Il flag session-{id} nel username ProxyHat garantisce una sticky session: tutte le richieste nello stesso context usano lo stesso IP residenziale. Quando chiudi il context e ne crei uno nuovo con un nuovo session ID, ottieni un IP fresco. Questo è essenziale per task multi-step come login, navigazione di categorie, e checkout.
Pattern di Scalabilità: Fleet Containerizzate e Browser Pool
Un singolo browser Puppeteer può gestire 5-10 tab concorrenti prima che il consumo di RAM diventi proibitivo (ogni tab ~80-150MB). Per lo scraping di produzione, hai bisogno di un browser pool con gestione del ciclo di vita.
Architettura di riferimento
Il pattern più efficace per lo web scraping a larga scala combina tre componenti:
- Job Queue (Redis/BullMQ) — distribuisce i task e gestisce i retry.
- Browser Pool Manager — mantiene un pool di browser contexts pronti, ciascuno con proxy e fingerprint dedicati.
- Worker Processes — consumano dalla coda, acquiscono un context, eseguono lo scrape, rilasciano il context.
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
const { EventEmitter } = require('events');
puppeteer.use(StealthPlugin());
class BrowserPool extends EventEmitter {
constructor({ maxBrowsers = 5, maxContextsPerBrowser = 3, proxyRotator }) {
super();
this.maxBrowsers = maxBrowsers;
this.maxContextsPerBrowser = maxContextsPerBrowser;
this.proxyRotator = proxyRotator;
this.browsers = [];
this.availableContexts = [];
}
async init() {
for (let i = 0; i < this.maxBrowsers; i++) {
const browser = await puppeteer.launch({
headless: 'new',
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu'
]
});
this.browsers.push({ browser, contexts: 0 });
}
}
async acquire() {
// Trova un browser con slot disponibili
const slot = this.browsers.find(
b => b.contexts < this.maxContextsPerBrowser
);
if (!slot) throw new Error('Pool esaurito');
const proxy = this.proxyRotator.next();
const context = await slot.browser.createBrowserContext({
proxyServer: proxy.server
});
const page = await context.newPage();
await page.authenticate({
username: proxy.username,
password: proxy.password
});
slot.contexts++;
this.emit('acquired', { proxy });
return {
context,
page,
proxy,
release: async () => {
await context.close();
slot.contexts--;
this.emit('released', { proxy });
}
};
}
async drain() {
await Promise.all(
this.browsers.map(({ browser }) => browser.close())
);
this.browsers = [];
}
}
// Utilizzo
const rotator = new ProxyRotator(['US', 'DE', 'GB', 'FR']);
const pool = new BrowserPool({
maxBrowsers: 4,
maxContextsPerBrowser: 3,
proxyRotator: rotator
});
(async () => {
await pool.init();
const urls = Array.from({ length: 12 }, (_, i) =>
`https://httpbin.org/ip?task=${i}`
);
const results = await Promise.all(
urls.map(async (url) => {
const handle = await pool.acquire();
try {
await handle.page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
const body = await handle.page.evaluate(() => document.body.innerText);
return { url, body };
} finally {
await handle.release();
}
})
);
console.log(`Completate ${results.length} richieste`);
await pool.drain();
})();Containerizzazione con Docker
Per il deployment in produzione, ogni worker gira in un container Docker con Chromium dedicato. Il pattern consigliato:
- 1 container = 1 worker = 1 browser instance — isolamento completo, crash di un container non influenza gli altri.
- Limita RAM a
--memory=2ge CPU a--cpus=1.5per prevenire memory leak di Chromium. - Usa
--disable-dev-shm-usageper evitare problemi con/dev/shmnei container. - Mount
/dev/shmcome tmpfs con dimensione adeguata (almeno 512MB).
# Dockerfile per Puppeteer worker
FROM node:20-slim
RUN apt-get update && apt-get install -y \
chromium \
fonts-liberation \
libappindicator3-1 \
libasound2 \
libatk-bridge2.0-0 \
libgtk-3-0 \
libnspr4 \
libnss3 \
libxss1 \
xdg-utils \
--no-install-recommends && \
rm -rf /var/lib/apt/lists/*
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
# Health check: verifica che il worker risponda
HEALTHCHECK --interval=30s --timeout=10s \
CMD node healthcheck.js || exit 1
CMD ["node", "worker.js"]Per l'orchestrazione, usa Kubernetes con Horizontal Pod Autoscaler: scala il numero di worker in base alla profondità della coda (metrica BullMQ). Imposta maxReplicas in base al tuo piano ProxyHat — controlla i limiti del tuo piano prima di scalare.
Gestione delle risorse e resilience
Alcune regole operative per la produzione:
- Restart browser ogni N richieste — Chromium ha memory leak noti. Dopo 50-100 richieste, chiudi il browser e lanciane uno nuovo.
- Timeout aggressivi — Imposta
timeout: 30000su goto e aspetta al massimo 15 secondi per selettori specifici. - Circuit breaker — Se 3 richieste consecutive falliscono con lo stesso IP, scarta il proxy e richiedine uno nuovo.
- Retry con backoff — Al primo fallimento, retry dopo 2s; al secondo, 4s; al terzo, scarta il task in una dead letter queue.
Nota Etica: Stealth per Scraping Legittimo, Non per Frode
Le tecniche descritte in questa guida sono potenti — e con la potenza arriva la responsabilità. Il Puppeteer anti-detection con proxy residenziali è legittimo quando:
- Raccogli dati pubblici per ricerca di mercato, monitoraggio prezzi, o verifica di contenuti.
- Esegui QA e testing dei tuoi stessi siti da diverse geolocazioni.
- Addestri modelli AI su dati pubblicamente accessibili rispettando
robots.txt.
Non è accettabile per:
- Creare account falsi, scalare ticket, o manipolare aste.
- Aggirare controlli di sicurezza per frode o phishing.
- Violare i Termini di Servizio di un sito per estrarre dati esplicitamente protetti.
Rispetta sempre robots.txt, i ToS del sito, e le normative applicabili (GDPR, CCPA). Lo scraping etico è un diritto — l'abuso non lo è.
Key Takeaways
- Il layer browser e il layer rete sono indipendenti — il plugin stealth patcha JavaScript, i proxy residenziali patchano l'identità di rete. Entrambi sono necessari.
- puppeteer-extra è architetturato come middleware — i plugin si compongono. Stealth + fingerprint randomizer + proxy rotation sono layers complementari, non alternativi.
- Randomizza le fingerprint per sessione — fingerprint identiche su migliaia di sessioni sono rilevabili quanto
navigator.webdriver = true.- Usa BrowserContext per isolamento — ogni context ha il proprio IP, cookie, e storage. È l'unità di lavoro corretta per la rotazione.
- Scala con container, non con tab — un browser per container, max 3-5 context per browser, restart periodico per memory leak.
- Lo stealth è uno strumento, non una licenza — usalo per scraping legittimo, rispetta robots.txt e ToS.
Per iniziare con proxy residenziali ottimizzati per Puppeteer, visita la pagina pricing di ProxyHat o esplora le locazioni disponibili per il geo-targeting globale.






