Guide complet : puppeteer-extra, le plugin Stealth et les proxies résidentiels

Apprenez à rendre Puppeteer indétectable en combinant puppeteer-extra-stealth, la rotation de proxies résidentiels et la randomisation des empreintes canvas/WebGL par session.

Guide complet : puppeteer-extra, le plugin Stealth et les proxies résidentiels

Pourquoi Puppeteer nu est facilement détectable

Si vous avez déjà lancé Puppeteer ou Playwright contre un site protégé, vous connaissez le scénario : la page se charge, puis un CAPTCHA apparaît, ou vous êtes redirigé vers une page « access denied ». Ce n'est pas de la magie — c'est de la détection d'automatisation.

Les systèmes anti-bot modernes (Cloudflare, Datadome, PerimeterX, Kasada) inspectent des dizaines de signaux côté navigateur. Voici les trois plus évidents que Puppeteer laisse derrière lui :

  • navigator.webdriver — Le protocole CDP (Chrome DevTools Protocol) définit automatiquement navigator.webdriver = true. C'est le signal le plus trivial à vérifier et presque tous les WAF le font.
  • Plugins et MIME types incohérents — Un Chrome normal expose navigator.plugins avec PDF Viewer, Chrome PDF Viewer, etc. Puppeteer-headless expose un tableau vide ou incomplet, ce qui est un marqueur immédiat.
  • Artefacts iframe chromedriver — Quand Puppeteer contrôle le navigateur via CDP, certaines pages injectent des iframes internes ou des variables window.cdc_ qui trahissent la présence de ChromeDriver. Les scripts de détection parcourent le DOM à la recherche de ces artefacts.

Mais ce n'est pas tout. Les systèmes avancés vérifient aussi :

  • L'absence de permissions navigator.permissions.query cohérentes avec un vrai navigateur.
  • Les dimensions de navigator.hardwareConcurrency et deviceMemory qui, sous headless, renvoient souvent des valeurs par défaut différentes.
  • L'ordre et la présence des en-têtes HTTP (ordre des headers, absence de Sec-CH-UA complet).
  • Les empreintes canvas et WebGL qui, sans randomisation, restent identiques entre les sessions — un signal de bot par excellence.

Un seul signal peut ne pas suffire à vous bloquer, mais les systèmes modernes combinent des dizaines de signaux en un score de risque. Réduire un seul vecteur ne suffit pas — il faut tous les couvrir.

puppeteer-extra et le plugin Stealth : ce qui est corrigé

puppeteer-extra est un wrapper autour de Puppeteer qui ajoute un système de plugins. Le plugin puppeteer-extra-plugin-stealth est une collection de modules (evasions) qui patchent les signaux d'automatisation les plus courants.

Voici les principaux sous-modules et ce qu'ils font :

Module d'évasionSignal corrigé
navigator.webdriverSupprime ou met à false la propriété navigator.webdriver
chrome.runtimeSimule la présence de window.chrome.runtime attendue dans Chrome
iframe.contentWindowCorrige les différences de contentWindow dans les iframes créées par CDP
media codecsRestaure les codecs média manquants en mode headless
navigator.pluginsInjecte les plugins standards (PDF Viewer, etc.)
navigator.languagesAssure que navigator.languages est cohérent avec les en-têtes HTTP
webgl.vendorCorrige le vendor/renderer WebGL qui diffère en headless
user-agent-overrideHarmonise le User-Agent avec les Client Hints

Installation et configuration de base :

npm install puppeteer puppeteer-extra puppeteer-extra-plugin-stealth
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');

// Activation du plugin stealth — applique les 9 evasions par défaut
puppeteer.use(StealthPlugin());

async function launchWithProxy() {
  const browser = await puppeteer.launch({
    headless: 'new',
    args: [
      '--no-sandbox',
      '--disable-setuid-sandbox',
      '--disable-blink-features=AutomationControlled',
      // Proxy résidentiel ProxyHat avec géo-ciblage France
      `--proxy-server=http://gate.proxyhat.com:8080`,
    ],
  });

  const page = await browser.newPage();
  // Authentification proxy via l'API CDP
  await page.authenticate({
    username: 'user-country-FR',
    password: 'PASSWORD',
  });

  await page.goto('https://bot.sannysoft.com/');
  // Vérifiez les résultats — la plupart des lignes doivent être vertes
  await page.screenshot({ path: 'stealth-check.png' });
  await browser.close();
}

launchWithProxy();

Notez l'argument --disable-blink-features=AutomationControlled — il est redondant avec le plugin stealth mais fournit une couche supplémentaire au niveau du lanceur Chromium.

Évasions sélectives

Parfois, vous ne voulez pas toutes les evasions — par exemple si votre cible vérifie spécifiquement l'absence d'une evasion. Vous pouvez désactiver des modules individuels :

const stealth = StealthPlugin();
// Désactiver l'évasion WebGL si vous gérez votre propre randomisation
stealth.enabledEvasions.delete('webgl.vendor');
stealth.enabledEvasions.delete('navigator.plugins');
puppeteer.use(stealth);

Combiner Stealth et proxies résidentiels : la pile anti-détection complète

Le plugin stealth corrige les signaux côté navigateur. Les proxies résidentiels corrigent les signaux côté réseau. Les deux sont nécessaires.

Pourquoi ? Même avec un navigateur parfaitement « propre », si votre IP sort d'un bloc datacenter (OVH, DigitalOcean, AWS), la plupart des WAF vous classeront comme bot. Les plages d'IP datacenter sont bien connues et souvent listées dans des bases de données comme IP2Location ou MaxMind.

Les proxies résidentiels de ProxyHat routent votre trafic via des IPs ISP réelles, ce qui rend votre empreinte réseau indiscernable d'un utilisateur légitime.

Architecture de la pile complète

  1. Couche réseau — Proxy résidentiel avec rotation par requête ou session sticky
  2. Couche navigateur — puppeteer-extra-stealth pour les evasions de base
  3. Couche empreinte — Randomisation canvas/WebGL personnalisée (section suivante)
  4. Couche comportementale — Délais aléatoires, mouvements de sourse simulés, scroll naturel

Avec ProxyHat, la rotation d'IP se gère directement dans le nom d'utilisateur :

// Rotation par requête — chaque requête obtient une nouvelle IP
const perRequestProxy = {
  username: 'user-country-US', // nouvelle IP à chaque connexion
  password: 'PASSWORD',
};

// Session sticky — même IP pendant toute la session
const stickyProxy = {
  username: 'user-country-US-session-mySession123',
  password: 'PASSWORD',
};

Randomisation canvas et WebGL par session

Le plugin stealth standard ne randomise pas les empreintes — il les corrige pour qu'elles ressemblent à celles d'un vrai navigateur. Mais si vous lancez 100 sessions depuis la même machine, elles auront toutes la même empreinte canvas. C'est un signal massif pour les systèmes de détection.

La solution : injecter du bruit contrôlé dans les opérations canvas et WebGL par session, en utilisant page.evaluateOnNewDocument avant que la page ne charge.

function createFingerprintNoise(seed) {
  // Générateur PRNG simple basé sur un seed de session
  const hash = (s) => {
    let h = 0;
    for (let i = 0; i < s.length; i++) {
      h = ((h << 5) - h + s.charCodeAt(i)) | 0;
    }
    return h;
  };
  const rng = () => {
    seed = (seed * 16807 + 0) % 2147483647;
    return (seed - 1) / 2147483646;
  };
  const noise = () => (rng() - 0.5) * 0.01; // ±0.005 de bruit

  return `
    // Bruit canvas — modifie subtilement les pixels rendus
    const _toDataURL = HTMLCanvasElement.prototype.toDataURL;
    HTMLCanvasElement.prototype.toDataURL = function(type) {
      const ctx = this.getContext('2d');
      if (ctx) {
        const imageData = ctx.getImageData(0, 0, this.width, this.height);
        for (let i = 0; i < imageData.data.length; i += 4) {
          imageData.data[i] += Math.round(${noise()});     // R
          imageData.data[i+1] += Math.round(${noise()});   // G
          imageData.data[i+2] += Math.round(${noise()});   // B
        }
        ctx.putImageData(imageData, 0, 0);
      }
      return _toDataURL.apply(this, arguments);
    };

    // Bruit WebGL — modifie les paramètres de rendu
    const _getParameter = WebGLRenderingContext.prototype.getParameter;
    WebGLRenderingContext.prototype.getParameter = function(param) {
      if (param === 0x1F00) { // VERSION
        return _getParameter.call(this, param) + ' (${seed})';
      }
      if (param === 0x9245) { // RENDERER
        return 'ANGLE (Generic GPU)';
      }
      return _getParameter.call(this, param);
    };
  `;
}

async function launchStealthSession(sessionId, proxyConfig) {
  const browser = await puppeteer.launch({
    headless: 'new',
    args: [
      '--no-sandbox',
      `--proxy-server=http://gate.proxyhat.com:8080`,
    ],
  });

  const page = await browser.newPage();
  await page.authenticate(proxyConfig);

  // Injecter le bruit d'empreinte AVANT le chargement de la page
  const noiseScript = createFingerprintNoise(hash(sessionId));
  await page.evaluateOnNewDocument(noiseScript);

  return { browser, page };
}

Chaque session reçoit un seed unique, ce qui produit une empreinte canvas/WebGL différente mais déterministe — si vous relancez la même session, l'empreinte reste cohérente, ce qui évite les incohérences intra-session que les détecteurs repèrent.

Rotation de proxy par contexte de navigateur

Puppeteer supporte les BrowserContexts — des contextes de navigation isolés au sein d'une même instance de navigateur. Chaque contexte a ses propres cookies, stockage et état. C'est idéal pour gérer plusieurs sessions simultanées avec des IPs différentes.

Cependant, l'argument --proxy-server s'applique au niveau du navigateur, pas du contexte. Pour une rotation par contexte, vous devez utiliser l'API CDP pour créer des sessions avec des proxies différents.

const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());

class ProxyRotator {
  constructor(proxyConfigs) {
    this.proxies = proxyConfigs;
    this.index = 0;
  }

  next() {
    const proxy = this.proxies[this.index % this.proxies.length];
    this.index++;
    return proxy;
  }
}

const rotator = new ProxyRotator([
  { username: 'user-country-US', password: 'PASSWORD' },
  { username: 'user-country-DE-city-berlin', password: 'PASSWORD' },
  { username: 'user-country-GB-city-london', password: 'PASSWORD' },
  { username: 'user-country-FR-city-paris', password: 'PASSWORD' },
]);

async function createContextWithProxy(browser, proxyConfig, sessionId) {
  const context = await browser.createIncognitoBrowserContext();
  const page = await context.newPage();

  // Auth proxy via CDP pour ce contexte
  await page.authenticate({
    username: proxyConfig.username,
    password: proxyConfig.password,
  });

  // Empreinte unique par session
  const noiseScript = createFingerprintNoise(hash(sessionId));
  await page.evaluateOnNewDocument(noiseScript);

  return { context, page };
}

async function runMultiContextCrawl(urls) {
  const browser = await puppeteer.launch({
    headless: 'new',
    args: [
      '--no-sandbox',
      `--proxy-server=http://gate.proxyhat.com:8080`,
    ],
  });

  const results = [];
  const concurrency = 4;

  for (let i = 0; i < urls.length; i += concurrency) {
    const batch = urls.slice(i, i + concurrency);
    const promises = batch.map((url, j) => {
      const proxy = rotator.next();
      const sessionId = `session-${i + j}-${Date.now()}`;
      return createContextWithProxy(browser, proxy, sessionId)
        .then(async ({ context, page }) => {
          try {
            await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
            const title = await page.title();
            results.push({ url, title, proxy: proxy.username });
          } catch (err) {
            results.push({ url, error: err.message });
          } finally {
            await context.close();
          }
        });
    });
    await Promise.allSettled(promises);
  }

  await browser.close();
  return results;
}

L'avantage de cette approche : vous ne lancez qu'une seule instance de Chromium, ce qui économise la mémoire, tout en isolant chaque session dans son propre contexte avec son proxy et son empreinte.

Mise à l'échelle : flottes conteneurisées et pools de navigateurs

Un seul navigateur Puppeteer, même avec des contextes multiples, a ses limites. Pour un crawling de production à grande échelle, vous avez besoin d'une architecture distribuée.

Architecture recommandée

  1. Worker containers — Chaque conteneur Docker exécute une instance Puppeteer avec 2-4 contextes. Limitez à ~4 contextes par instance pour éviter les fuites mémoire.
  2. Queue de tâches — Redis ou RabbitMQ comme file d'attente de URLs à crawler.
  3. Proxy pool manager — Service centralisé qui attribue des proxies aux workers, gère les rotations et surveille le taux de succès par IP.
  4. Orchestrateur — Kubernetes ou Docker Swarm pour scaler les workers horizontalement.
// worker.js — Processus worker individuel
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
const Redis = require('ioredis');

puppeteer.use(StealthPlugin());

const MAX_CONTEXTS = 4;
const MEMORY_LIMIT_MB = 512;

function getMemoryUsageMB() {
  const used = process.memoryUsage().heapUsed;
  return used / 1024 / 1024;
}

async function startWorker() {
  const redis = new Redis(process.env.REDIS_URL);
  const browser = await puppeteer.launch({
    headless: 'new',
    args: [
      '--no-sandbox',
      '--disable-setuid-sandbox',
      '--disable-dev-shm-usage', // crucial dans Docker
      '--disable-gpu',
      `--proxy-server=http://gate.proxyhat.com:8080`,
      `--memory-pressure-off`,
    ],
  });

  let activeContexts = 0;

  while (true) {
    if (getMemoryUsageMB() > MEMORY_LIMIT_MB) {
      console.log('Limite mémoire atteinte, redémarrage du navigateur');
      await browser.close();
      // Le process manager (PM2/K8s) relancera le worker
      process.exit(0);
    }

    if (activeContexts >= MAX_CONTEXTS) {
      await new Promise(r => setTimeout(r, 1000));
      continue;
    }

    const task = await redis.lpop('crawl:queue');
    if (!task) {
      await new Promise(r => setTimeout(r, 2000));
      continue;
    }

    const { url, geo } = JSON.parse(task);
    activeContexts++;

    const sessionId = `w${process.pid}-${Date.now()}`;
    const proxyConfig = {
      username: `user-country-${geo}-session-${sessionId}`,
      password: process.env.PROXY_PASSWORD,
    };

    createContextWithProxy(browser, proxyConfig, sessionId)
      .then(async ({ context, page }) => {
        try {
          await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
          const html = await page.content();
          await redis.rpush('crawl:results', JSON.stringify({
            url, geo, sessionId, htmlLength: html.length, ok: true,
          }));
        } catch (err) {
          await redis.rpush('crawl:results', JSON.stringify({
            url, geo, sessionId, error: err.message, ok: false,
          }));
          // Rejetter les tâches échouées pour retry
          if (err.message.includes('net::ERR')) {
            await redis.rpush('crawl:retry', task);
          }
        } finally {
          await context.close();
          activeContexts--;
        }
      });
  }
}

startWorker().catch(console.error);

Dockerfile optimisé pour Puppeteer

FROM node:20-slim

# Dépendances Chromium + polices
RUN apt-get update && apt-get install -y \
    chromium \
    fonts-liberation \
    fonts-noto-color-emoji \
    --no-install-recommends && \
    rm -rf /var/lib/apt/lists/*

ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true

WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .

# Sécurité : pas de root
RUN groupadd -r pptruser && useradd -r -g pptruser pptruser
USER pptruser

CMD ["node", "worker.js"]

Surveillance et métriques clés

Pour gérer une flotte de crawlers, vous devez monitorer :

  • Taux de succès par proxy — Si une IP a < 70% de succès, retirez-la temporairement.
  • Latence moyenne par géo — Les proxies résidentiels ont une latence plus élevée que les datacenter ; surveillez les anomalies.
  • Consommation mémoire par worker — Les fuites mémoire de Chromium sont réelles ; redémarrez les workers avant le crash.
  • Nombre de CAPTCHAs rencontrés — Un pic indique que votre stealth est insuffisant ou que votre IP est flaguée.

Note éthique : le stealth est pour le scraping légitime, pas la fraude

Les techniques décrites dans cet article sont des outils puissants. Avec un grand pouvoir vient une grande responsabilité.

  • Respectez le robots.txt — Même si vous pouvez techniquement contourner les restrictions, vérifiez toujours les directives de crawl du site.
  • Respectez les conditions d'utilisation — Le scraping de données publiques est légal dans de nombreuses juridictions, mais violer les ToS peut avoir des conséquences.
  • Conformité RGPD/CCPA — Ne scrapez jamais de données personnelles sans base légale. Les données publiques ne signifient pas automatiquement des données libres d'utilisation.
  • Rate limiting éthique — Même avec des proxies résidentiels, limitez votre taux de requêtes pour ne pas surcharger les serveurs cibles.
  • Pas de fraude — L'utilisation de ces techniques pour contourner les files d'attente de billets, manipuler les prix, ou commettre une fraude est illégale et immorale.

Chez ProxyHat, nous encourageons l'utilisation responsable de nos proxies résidentiels pour le web scraping, le suivi SERP, la recherche et l'analyse de données.

Points clés à retenir

Key Takeaways

  • Puppeteer nu est détectable via navigator.webdriver, les plugins vides, et les artefacts chromedriver — le plugin stealth corrige ces signaux.
  • Le stealth seul ne suffit pas : sans proxy résidentiel, votre IP datacenter vous trahit au niveau réseau.
  • La randomisation canvas/WebGL par session est essentielle pour éviter les empreintes identiques entre sessions.
  • Les BrowserContexts permettent la rotation de proxy par session dans une seule instance Chromium.
  • Pour la production, utilisez des workers conteneurisés avec une queue Redis, un gestionnaire de pool de proxies et un orchestrateur K8s.
  • Le monitoring du taux de succès, de la latence et de la mémoire est indispensable pour une flotte stable.
  • Utilisez toujours ces techniques de manière éthique et conforme à la loi.

FAQ

Les réponses aux questions les plus fréquentes sur puppeteer-extra, le plugin stealth et les proxies :

Prêt à commencer ?

Accédez à plus de 50M d'IPs résidentielles dans plus de 148 pays avec filtrage IA.

Voir les tarifsProxies résidentiels
← Retour au Blog