Resolver problemas do Puppeteer

O Headless Chrome não é iniciado no Windows

Algumas políticas do Chrome podem exigir a execução do Chrome ou do Chromium com determinadas extensões.

O Puppeteer transmite a sinalização --disable-extensions por padrão e, portanto, não é iniciada quando essas políticas estão ativas.

Para contornar esse problema, execute sem a flag:

const browser = await puppeteer.launch({
  ignoreDefaultArgs: ['--disable-extensions'],
});

Contexto: problema 3681 (link em inglês).

O Headless Chrome não é lançado no UNIX

Verifique se todas as dependências necessárias estão instaladas. Execute ldd chrome | grep not em uma máquina Linux para verificar quais dependências estão ausentes.

Dependências do Debian (Ubuntu)

ca-certificates
fonts-liberation
libappindicator3-1
libasound2
libatk-bridge2.0-0
libatk1.0-0
libc6
libcairo2
libcups2
libdbus-1-3
libexpat1
libfontconfig1
libgbm1
libgcc1
libglib2.0-0
libgtk-3-0
libnspr4
libnss3
libpango-1.0-0
libpangocairo-1.0-0
libstdc++6
libx11-6
libx11-xcb1
libxcb1
libxcomposite1
libxcursor1
libxdamage1
libxext6
libxfixes3
libxi6
libxrandr2
libxrender1
libxss1
libxtst6
lsb-release
wget
xdg-utils

Dependências do CentOS

alsa-lib.x86_64
atk.x86_64
cups-libs.x86_64
gtk3.x86_64
ipa-gothic-fonts
libXcomposite.x86_64
libXcursor.x86_64
libXdamage.x86_64
libXext.x86_64
libXi.x86_64
libXrandr.x86_64
libXScrnSaver.x86_64
libXtst.x86_64
pango.x86_64
xorg-x11-fonts-100dpi
xorg-x11-fonts-75dpi
xorg-x11-fonts-cyrillic
xorg-x11-fonts-misc
xorg-x11-fonts-Type1
xorg-x11-utils

Depois de instalar as dependências, atualize a biblioteca nss usando este comando

yum update nss -y

Confira as discussões:

  • #290: solução de problemas no Debian
  • #391: solução de problemas do CentOS
  • N.o 379: solução de problemas alpino

O Headless Chrome desativa a composição de GPU

O Chrome e o Chromium exigem que --use-gl=egl ative a aceleração de GPU no modo headless.

const browser = await puppeteer.launch({
  headless: true,
  args: ['--use-gl=egl'],
});

O download do Chrome é feito, mas não é possível iniciar no Node.js.

Se você receber um erro parecido com este ao tentar iniciar o Chromium:

(node:15505) UnhandledPromiseRejectionWarning: Error: Failed to launch the browser process!
spawn /Users/.../node_modules/puppeteer/.local-chromium/mac-756035/chrome-mac/Chromium.app/Contents/MacOS/Chromium ENOENT

Isso significa que o download do navegador foi feito, mas ocorreu uma falha na extração correta. A causa mais comum é um bug no Node.js v14.0.0 que corrompeu o extract-zip, o módulo usado pelo Puppeteer para extrair os downloads do navegador para o lugar certo. O bug foi corrigido no Node.js v14.1.0. Portanto, verifique se você está executando essa versão ou mais recente.

Configurar um sandbox do Chrome Linux

Para proteger o ambiente do host contra conteúdo da Web não confiável, o Chrome usa várias camadas de sandbox. Para que isso funcione corretamente, o host deve ser configurado primeiro. Se não houver um sandbox bom para o Chrome usar, ele vai falhar com o erro No usable sandbox!.

Se você confiança absolutamente no conteúdo aberto no Chrome, inicie o navegador com o argumento --no-sandbox:

const browser = await puppeteer.launch({
  args: ['--no-sandbox', '--disable-setuid-sandbox'],
});

Há duas maneiras de configurar um sandbox no Chromium.

A clonagem de namespace do Sser só é compatível com kernels modernos. Namespaces de usuários sem privilégios geralmente podem ser ativados, mas podem abrir mais superfície de ataque do kernel para processos não raiz (sem sandbox) para elevar a privilégios do kernel.

sudo sysctl -w kernel.unprivileged_userns_clone=1

[alternativa] Configurar o sandbox setuid

O sandbox setuid é disponibilizado como um executável autônomo e está localizado ao lado do Chromium que o Puppeteer faz o download. Não há problema em reutilizar o mesmo executável de sandbox para diferentes versões do Chromium. Assim, o seguinte só pode ser feito uma vez por ambiente de host:

# cd to the downloaded instance
cd <project-dir-path>/node_modules/puppeteer/.local-chromium/linux-<revision>/chrome-linux/
sudo chown root:root chrome_sandbox
sudo chmod 4755 chrome_sandbox
# copy sandbox executable to a shared location
sudo cp -p chrome_sandbox /usr/local/sbin/chrome-devel-sandbox
# export CHROME_DEVEL_SANDBOX env variable
export CHROME_DEVEL_SANDBOX=/usr/local/sbin/chrome-devel-sandbox

Você pode exportar a variável de ambiente CHROME_DEVEL_SANDBOX por padrão. Nesse caso, adicione o seguinte a ~/.bashrc ou .zshenv:

export CHROME_DEVEL_SANDBOX=/usr/local/sbin/chrome-devel-sandbox

Executar o Puppeteer no Travis CI

Executamos nossos testes para o Puppeteer no Travis CI até a v6.0.0. Depois disso, migramos para o GitHub Actions. Consulte .travis.yml (v5.5.0) para referência.

Confira algumas práticas recomendadas:

  • O serviço xvfb precisa ser iniciado para executar o Chromium no modo não headless.
  • É executado no Xenial Linux no Travis por padrão.
  • Executa npm install por padrão
  • node_modules é armazenado em cache por padrão

.travis.yml pode ter esta aparência:

language: node_js
node_js: node
services: xvfb

script:
  - npm run test

Executar o Puppeteer no CircleCI

  1. Comece com uma imagem NodeJS na configuração. yaml docker: - image: circleci/node:14 # Use your desired version environment: NODE_ENV: development # Only needed if puppeteer is in `devDependencies`
  2. Dependências como libXtst6 provavelmente precisam ser instaladas com apt-get. Portanto, use o esfera threetreeslight/puppeteer (instructions) ou cole partes da fonte na sua própria configuração.
  3. Por fim, se você usa o Puppeteer com o Jest, pode encontrar um erro que gera processos filhos: shell [00:00.0] jest args: --e2e --spec --max-workers=36 Error: spawn ENOMEM at ChildProcess.spawn (internal/child_process.js:394:11) Isso provavelmente é causado pela detecção automática do número de processos em toda a máquina (36) em vez do número permitido no contêiner (2). Para corrigir isso, defina jest --maxWorkers=2 no comando de teste.

Executar o Puppeteer no Docker

Pode ser complicado instalar o Chrome headless no Docker. O Chromium empacotado que o Puppeteer instala não tem as dependências necessárias da biblioteca compartilhada.

Para corrigir, você precisa instalar as dependências que faltam e o pacote mais recente do Chromium no Dockerfile:

FROM node:14-slim

# Install latest chrome dev package and fonts to support major charsets (Chinese, Japanese, Arabic, Hebrew, Thai and a few others)
# Note: this installs the necessary libs to make the bundled version of Chromium that Puppeteer
# installs, work.
RUN apt-get update \
    && apt-get install -y wget gnupg \
    && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
    && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
    && apt-get update \
    && apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \
      --no-install-recommends \
    && rm -rf /var/lib/apt/lists/*

# If running Docker >= 1.13.0 use docker run's --init arg to reap zombie processes, otherwise
# uncomment the following lines to have `dumb-init` as PID 1
# ADD https://github.com/Yelp/dumb-init/releases/download/v1.2.2/dumb-init_1.2.2_x86_64 /usr/local/bin/dumb-init
# RUN chmod +x /usr/local/bin/dumb-init
# ENTRYPOINT ["dumb-init", "--"]

# Uncomment to skip the chromium download when installing puppeteer. If you do,
# you'll need to launch puppeteer with:
#     browser.launch({executablePath: 'google-chrome-stable'})
# ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true

# Install puppeteer so it's available in the container.
RUN npm init -y &&  \
    npm i puppeteer \
    # Add user so we don't need --no-sandbox.
    # same layer as npm install to keep re-chowned files from using up several hundred MBs more space
    && groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \
    && mkdir -p /home/pptruser/Downloads \
    && chown -R pptruser:pptruser /home/pptruser \
    && chown -R pptruser:pptruser /node_modules \
    && chown -R pptruser:pptruser /package.json \
    && chown -R pptruser:pptruser /package-lock.json

# Run everything after as non-privileged user.
USER pptruser

CMD ["google-chrome-stable"]

Criar o contêiner:

docker build -t puppeteer-chrome-linux .

Execute o contêiner passando node -e "<yourscript.js content as a string>" como o comando:

 docker run -i --init --rm --cap-add=SYS_ADMIN \
   --name puppeteer-chrome puppeteer-chrome-linux \
   node -e "`cat yourscript.js`"

Há um exemplo completo em https://github.com/ebidel/try-puppeteer (link em inglês) que mostra como executar esse Dockerfile a partir de um servidor da Web em execução no ambiente flexível do App Engine (Node).

Executar no Alpine

O pacote Chromium mais recente com suporte no Alpine é o 100, que corresponde ao Puppeteer v13.5.0.

Exemplo de Dockerfile:

FROM alpine

# Installs latest Chromium (100) package.
RUN apk add --no-cache \
      chromium \
      nss \
      freetype \
      harfbuzz \
      ca-certificates \
      ttf-freefont \
      nodejs \
      yarn

...

# Tell Puppeteer to skip installing Chrome. We'll be using the installed package.
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
    PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser

# Puppeteer v13.5.0 works with Chromium 100.
RUN yarn add puppeteer@13.5.0

# Add user so we don't need --no-sandbox.
RUN addgroup -S pptruser && adduser -S -G pptruser pptruser \
    && mkdir -p /home/pptruser/Downloads /app \
    && chown -R pptruser:pptruser /home/pptruser \
    && chown -R pptruser:pptruser /app

# Run everything after as non-privileged user.
USER pptruser

...

Práticas recomendadas com o Docker

Por padrão, o Docker executa um contêiner com 64 MB de espaço de memória compartilhada do /dev/shm. Esse valor geralmente é muito pequeno para o Chrome e faz com que ele falhe ao renderizar páginas grandes. Para corrigir, execute o contêiner com docker run --shm-size=1gb para aumentar o tamanho de /dev/shm. Desde o Chrome 65, isso não é mais necessário. Em vez disso, inicie o navegador com a flag --disable-dev-shm-usage:

const browser = await puppeteer.launch({
  args: ['--disable-dev-shm-usage'],
});

Isso grava arquivos de memória compartilhada em /tmp em vez de /dev/shm. Consulte crbug.com/736452 (link em inglês).

Você encontrou outros erros estranhos ao iniciar o Chrome? Tente executar seu contêiner com docker run --cap-add=SYS_ADMIN ao desenvolver localmente. Como o Dockerfile adiciona um usuário pptr como sem privilégios, é possível que ele não tenha todos os privilégios necessários.

Vale a pena conferir o dumb-init caso você esteja passando por vários processos zumbis que persistem no Chrome. Há tratamento especial para processos com PID=1, o que dificulta o encerramento correto do Chrome em alguns casos (como com o Docker).

Executar o Puppeteer na nuvem

No Google App Engine

O ambiente de execução do Node.js do ambiente padrão do App Engine vem com todos os pacotes de sistema necessários para executar o Headless Chrome.

Para usar puppeteer, liste o módulo como uma dependência em package.json e implante no Google App Engine. Leia mais sobre como usar o puppeteer no App Engine seguindo o tutorial oficial.

No Google Cloud Functions

O ambiente de execução do Node.js 10 do Google Cloud Functions vem com todos os pacotes de sistema necessários para executar o Headless Chrome.

Para usar puppeteer, liste o módulo como uma dependência em package.json e implante a função no Google Cloud Functions usando o ambiente de execução nodejs10.

Executar o Puppeteer no Google Cloud Run

O ambiente de execução padrão do Node.js do Google Cloud Run não vem com os pacotes de sistema necessários para executar o Headless Chrome. Configure seu próprio Dockerfile e inclua as dependências ausentes.

No Heroku

A execução do Puppeteer no Heroku requer algumas dependências adicionais que não estão incluídas na caixa do Linux que o Heroku gera. Para adicionar as dependências na implantação, adicione o buildpack do Puppeteer Heroku à lista de pacotes de criação do aplicativo em Configurações > Buildpacks.

O URL do buildpack é https://github.com/jontewks/puppeteer-heroku-buildpack

Use o modo '--no-sandbox' ao iniciar o Puppeteer. Isso pode ser feito transmitindo-o como um argumento para a chamada de .launch(): puppeteer.launch({ args: ['--no-sandbox'] });.

Ao clicar em "Adicionar buildpack", cole o URL na entrada e clique em Salvar. Na próxima implantação, o aplicativo também instalará as dependências que o Puppeteer precisa executar.

Se você precisa renderizar caracteres chineses, japoneses ou coreanos, pode ser necessário usar um buildpack com arquivos de fonte adicionais, como https://github.com/CoffeeAndCode/puppeteer-heroku-buildpack

Há também outro guia do @timleland que inclui um projeto de exemplo.

No AWS Lambda

O AWS Lambda limita os tamanhos dos pacotes de implantação para aproximadamente 50 MB. Isso apresenta desafios para a execução da versão headless do Chrome (e, portanto, do Puppeteer) no Lambda. A comunidade reuniu alguns recursos que abordam os problemas:

Instância do AWS EC2 executando o Amazon-Linux

Se você tiver uma instância do EC2 que executa o amazon-linux no pipeline de CI/CD e quiser executar testes do Puppeteer no amazon-linux, siga estas etapas.

  1. Para instalar o Chromium, primeiro ative amazon-linux-extras, que faz parte do EPEL (Extra Packages for Enterprise Linux):

    sudo amazon-linux-extras install epel -y
    
  2. Em seguida, instale o Chromium:

    sudo yum install -y chromium
    

Agora o Puppeteer pode iniciar o Chromium para executar os testes. Se você não ativar o EPEL e continuar instalando o Chromium como parte de npm install, o Puppeteer não poderá iniciar o Chromium devido à indisponibilidade de libatk-1.0.so.0 e de muitos outros pacotes.

Problemas de transpilação de código

Se você estiver usando um transpilador JavaScript, como babel ou TypeScript, é possível que a chamada de evaluate() com uma função assíncrona não funcione. Isso ocorre porque puppeteer usa Function.prototype.toString() para serializar funções, enquanto os transpiladores podem alterar o código de saída de modo que ele seja incompatível com puppeteer.

Algumas soluções alternativas para esse problema são instruir o transpilador a não confundir o código. Por exemplo, configurar o TypeScript para usar a versão mais recente do ecma ("target": "es2018"). Outra solução alternativa é usar modelos de string em vez de funções:

await page.evaluate(`(async() => {
   console.log('1');
})()`);