#781·camoufox

EPIPE crash in Playwright Node driver when Google OAuth popup closes

Author: ras-indoCreated Sep 23, 2026Updated Sep 23, 2026

Bug Report: Google OAuth Popup Crashes with EPIPE in Camoufox

Summary

When using Camoufox with Playwright's Python API, Google OAuth popup flows cause an EPIPE crash in the Playwright Node.js driver. The popup opens, but after the user clicks their Google account, the popup closes within ~2.5 seconds before the consent page can load, and the Playwright driver crashes with an unhandled EPIPE error on the PipeTransport.send() method.

This prevents any Google OAuth login flow (TikTok, etc.) from completing in headless or headed mode.

Environment

  • Camoufox version: 0.5.6 (pip camoufox[geoip])
  • Camoufox browser: v152.0.4-beta.30 (aarch64-linux)
  • Playwright version: 1.62.0 (bundled with Camoufox)
  • Python: 3.11
  • OS: Linux aarch64 (Armbian on Amlogic S905X)
  • Node.js: v24.18.0
  • Display: Xvfb (virtual framebuffer for headed mode)

Steps to Reproduce

python
from camoufox.sync_api import Camoufox
import time

with Camoufox(
    headless=True,  # or headless=False with DISPLAY=:99
    os='windows',
    humanize=True,
    geoip=True,
    main_world_eval=True,
    persistent_context=True,
    user_data_dir='/tmp/test-profile',
    i_know_what_im_doing=True,
    disable_coop=True
) as browser:
    page = browser.new_page()
    page.goto('https://www.tiktok.com/login', wait_until='networkidle', timeout=45000)
    page.wait_for_timeout(15000)
    
    # Click "Continue with Google" button
    page.evaluate('''() => {
        const els = document.querySelectorAll('div[data-e2e="channel-item"]');
        for (const el of els) {
            if ((el.textContent||'').includes('Google')) { el.click(); return; }
        }
    }''')
    
    page.wait_for_timeout(10000)
    # At this point, EPIPE crash occurs

Expected Behavior

  1. Google OAuth popup opens
  2. User selects account
  3. Consent page loads
  4. User clicks "Allow"/"Lanjutkan"
  5. Popup closes after token is sent via postMessage
  6. Parent page receives token and sets cookies
  7. No crash occurs

Actual Behavior

  1. Google OAuth popup opens ✅
  2. User account is clicked ✅
  3. Popup closes within ~2.5 seconds ❌
  4. Consent page never loads ❌
  5. EPIPE crash occurs in Playwright Node.js driver ❌

Error Stack Trace

node:events:487
      throw er; // Unhandled 'error' event
      ^

Error: write EPIPE
    at afterWriteDispatched (node:internal/stream_base_commons:159:15)
    at writeGeneric (node:internal/streams/writable:389:12)
    at _write (node:internal/streams/writable:330:10)
    at Socket.Writable.write (node:internal/streams/writable:334:10)
    at PipeTransport.send (coreBundle.js:34569:25)
    at dispatcherConnection.onmessage (coreBundle.js:69266:59)
    at DispatcherConnection.sendEvent (coreBundle.js:18641:14)
Emitted 'error' event on Socket instance at:
    at emitErrorNT (node:internal/stream_base.destroy:157:8)
    at emitErrorCloseNT (node:internal/stream_base.destroy:122:3)
    at process.processTicksAndRejections (node:internal/process/task_queues:83:21) {
  errno: -32,
  syscall: 'write',
  code: 'EPIPE'
}

Root Cause Analysis

The EPIPE Crash

The crash occurs in PipeTransport.send() at coreBundle.js:34569. When the Google OAuth popup page is destroyed during navigation (account click → consent page redirect), Playwright's dispatcher tries to send a __dispose__ event to the Python client. However, the pipe between the Node.js driver and the Python process is already closed, causing the EPIPE error.

The error is emitted as an event on the Socket, not thrown by the write() call. This means:

  • try/catch around pipeWrite.write() does NOT catch it
  • The error is emitted asynchronously by Node.js's Writable stream
  • The only way to handle it is via socket.on('error', ...) handler

The Popup Lifecycle Issue

The popup closes within ~2.5 seconds after the account click. This is observed in the monitoring output:

0.0s: 3 pages: ['about:blank', 'https://www.tiktok.com/login', 'https://accounts.google.com/...']
2.5s: 2 pages: ['about:blank', 'https://www.tiktok.com/login']

The Google popup page disappears before the consent page can fully load. This may be caused by:

  1. Google detecting the automated browser and closing the popup
  2. Camoufox's Juggler protocol not properly handling popup navigation
  3. A timing issue in how Camoufox manages popup page lifecycle

What We've Tried

Approach Result
disable_coop=True EPIPE still crashes
headless=False + Xvfb EPIPE still crashes
Mobile UA + viewport Still popup flow, same crash
EPIPE handler via NODE_OPTIONS Prevents crash but connection broken
Patch PipeTransport.send() try/catch Doesn't catch (error is async event)
Patch sendDispose() Doesn't catch (error is async event)
Patch sendEvent() Doesn't catch (error is async event)
Add socket.on('error') handler Prevents crash but consent still doesn't load

Impact

  • Critical: Any Google OAuth login flow via popup is completely broken
  • Affects all sites that use Google OAuth with popup flow (TikTok, etc.)
  • The issue is specific to Camoufox's Juggler protocol handling of popup pages

Additional Context

Related Issues

  • #635: Juggler emits Page.uncaughtError without location, crashing Playwright Node driver. This is related but different — our crash is in PipeTransport.send(), not in error handling.
  • Playwright #42390: Firefox launcher forces -juggler-pipe which breaks Camoufox on Windows. Related to Juggler compatibility.
  • Playwright #32081: Google login not working in Playwright Firefox. Confirms Google actively blocks automated browsers.

What Works

  • Camoufox itself works perfectly for non-OAuth tasks
  • Google login via MyAccount (direct navigation, not OAuth popup) works
  • Persistent profiles work correctly
  • Anti-detection features work (CreepJS, BrowserScan pass)

Possible Fixes

  1. Fix popup lifecycle in Juggler: When a popup page navigates, ensure the old page context is properly cleaned up before sending dispose events
  2. Add error handler to PipeTransport: Similar to PipeTransport2 which has pipeWrite.on("error", ...), add the same handler to PipeTransport
  3. Fix the EPIPE in send(): The write() call should be wrapped in error handling that catches async stream errors
  4. Investigate why popup closes in 2.5s: This may be a Google detection issue or a Juggler protocol issue

Labels

  • bug
  • playwright
  • google-oauth
  • popup
  • EPIPE
  • critical