Skip to content

fix: X blocks bundled headless Chromium with 403 - #18

Merged
RichardBray merged 4 commits into
mainfrom
fix/x-headless-chromium-403
Sep 3, 2026
Merged

RichardBray merged 4 commits into
mainfrom
fix/x-headless-chromium-403

Conversation

@RichardBray

@RichardBray RichardBray commented Sep 3, 2026 •

Copy link
Copy Markdown
Owner

Problem

Video extraction was failing for all tweets with "Failed to extract video URL", even for public videos with no login wall.

Root cause: X returns HTTP 403 to every request from Playwright's bundled headless Chromium, regardless of User-Agent. page.goto() never actually loaded the tweet page, so it was never a video-detection problem — the page was blank.

Confirmed via direct testing:

  • Bundled Chromium + any UA → 403, always
  • Real installed Chrome (channel: 'chrome') + its default UA (which literally contains HeadlessChrome/...) → still blocked
  • Real Chrome + a normal desktop Chrome UA string → 200, consistently

Fix

In src/extractor.ts:

  1. Set a normal desktop Chrome userAgent on the context (removes the HeadlessChrome tell).
  2. When no --browser-channel/--browser-executable-path is explicitly passed, prefer a locally installed real Chrome (reusing findChromePath() from private.ts) over bundled Chromium, falling back to bundled Chromium if Chrome isn't found.

Testing

Verified against two previously-failing tweet URLs — video extracted and downloaded successfully for both:

  • https://x.com/mattshumer_/status/2095187868746383758
  • https://x.com/bridgemindai/status/2094894312370692443

Full test suite passes (61 pass / 9 skip).

🤖 Generated with Claude Code

https://claude.ai/code/session_014i3FdrzgdTStsSytFiyzyV


Devin Review

Playwright's bundled headless Chromium gets a 403 from x.com on every
request, regardless of User-Agent. Fix by:
- setting a normal desktop Chrome UA on the context (removes the
  HeadlessChrome tell)
- preferring a locally installed real Chrome (channel: chrome) over
  bundled Chromium when no explicit --browser-channel/--browser-executable-path
  is given, since X fingerprints the bundled binary itself and blocks
  it outright even with a spoofed UA

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014i3FdrzgdTStsSytFiyzyV

@devin-ai-integration devin-ai-integration Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

Devin Review found 1 potential issue.

Devin Review

Comment thread src/extractor.ts Outdated
Comment on lines +155 to +156
if (findChromePath()) {
launchOptions.channel = 'chrome';

@devin-ai-integration devin-ai-integration Bot Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Chrome-free installations remain unusable

Without Google Chrome, findChromePath() leaves blocked bundled Chromium selected or mistakes system Chromium for Chrome. Every extraction then fails with a 403 or launch error.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

- Use static import of findChromePath instead of dynamic await import
- Use executablePath when Chrome found, not channel: 'chrome' (more robust)
- Update hardcoded UA to macOS 14 (Sonoma) + Chrome 132 (realistic pairing)
- Add comment explaining UA needs periodic updates
- Add Windows support to findChromePath with common installation paths
- Simplify comments to explain 'why' not 'what'

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014i3FdrzgdTStsSytFiyzyV
devin-ai-integration[bot]

This comment was marked as resolved.

Richard Oliver Bray and others added 2 commits September 3, 2026 16:24
Devin Review correctly flagged that the CDP and login-retry flows
(x-dl cdp, and the automatic login-wall retry) launch their own
headless Chrome via launchPrivateBrowser, bypassing
createContextAndPage entirely. That context never got the desktop UA
override, so it kept the default HeadlessChrome UA and hit the same
403 the original fix addressed, just on a different path.

Move DESKTOP_CHROME_UA into private.ts and apply it in
launchPrivateBrowser whenever running headless (a headed, user-visible
login flow doesn't need the spoof). extractor.ts now imports the
constant instead of duplicating it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014i3FdrzgdTStsSytFiyzyV
createContextAndPage was always applying DESKTOP_CHROME_UA, including
in headed mode, while launchPrivateBrowser only applies it when
headless. This was inconsistent: headed browsers (used for login or
user testing) don't need and shouldn't need UA spoofing. Both now
apply the override only in headless mode.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014i3FdrzgdTStsSytFiyzyV
@RichardBray
RichardBray merged commit 3e18ee8 into main Sep 3, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant