Three Playwright/Apify bugs that took me way too long to find (and the fixes)
Three Playwright/Apify Bugs That Took Me Way Too Long to Find (and the Fixes)
I recently shipped a small Apify Actor - a keyword-based monitor for a government data portal, built with Playwright + Camoufox (a hardened Firefox build for stealth automation). It works, and it's live: it turned out to be a much better debugging exercise than I expected. Three bugs in particular cost me way more time than they should have, so I'm writing them down for whoever hits the same wall next.
A Silent Version Mismatch Between Playwright and Camoufox
Right after wiring up the browser launch, every run failed with:
Found property ".viewport.isMobile" - false which is not described in this scheme
Nothing in my code touched isMobile. Turned out Playwright 1.61 added that field to the internal Browser.setDefaultViewport protocol call, and Camoufox's Juggler layer (the protocol it uses to drive Firefox) didn't recognize it yet. It's a real upstream incompatibility, tracked in daijro/camoufox#653.
The counter-intuitive part: the official Apify base image is tagged apify-python-playwright-camoufox:3.14-1.61.0 - so the natural instinct is to match your Playwright pin to 1.61.0. Don't. That's the broken version for this combo. Pinning playwright==1.60.0 fixed it immediately.
Lesson: when a base image tag looks like a version recommendation, verify it against the actual compatibility matrix of the libraries involved - it can just be a taxonomy label, not a peer-dependency promise.
The .click() on a Text Input Can Hang Forever; .fill() Doesn't
This one took the longest to nail down because it was intermittent. Some runs would sail through in 40 seconds; others would sit at Locator.click() until a timeout fired, with no error, no popup, nothing actionable in the trace. After enough repeated runs, I noticed the timeouts always stalled at the exact same call: clicking a text input immediately before typing into it - a completely standard "click to focus, then type" pattern.
Switching to locator.fill(value) - which focuses the element as part of its own actionability checks, without dispatching a raw mouse click - made the hang disappear across dozens of subsequent runs.
I never found a fully satisfying root cause (my best guess: something in the click-dispatch path occasionally not resolving cleanly under a residential-proxy connection with irregular latency, which fill()'s internal path avoids). But the fix generalizes: if you don't need the literal mouse-click side effect, .fill() is strictly more robust for text inputs - one less network round-trip and no dependency on precise coordinate/z-index resolution to succeed.
Apify's Automated Actor Tests Use Your Default Input - and an Empty Dataset Counts as a Failure
Apify runs every published Actor daily with its default/prefilled input and expects:
- Success
- Completion within 5 minutes
- A non-empty dataset
Miss any of those on 2 of the last 3 daily runs and the Actor gets silently flagged "Under maintenance" (hidden from Store search) - you find out by email.
Two non-obvious ways to trip this that aren't really "bugs" in the traditional sense:
- If your default input is a narrow query that occasionally has zero real-world matches, a perfectly correct run with zero results still counts as a failure. Pick a default keyword/filter broad enough to (almost) always return something.
- If you've added retry logic to make individual runs more resilient (good for real users, who don't mind an extra 2 minutes), make sure the worst case - including retries - still fits inside 5 minutes. A retry loop tuned for "eventually succeed" and a hard 5-minute CI-style SLA pull in different directions; you have to explicitly budget for both.
Also: Pick a Genuinely Persistent Store, Not the Default One
Small one, but it cost a day of "why isn't my session persisting" confusion: Actor.getValue() / Actor.setValue() operate on the current run's default key-value store, which is fresh every run. To actually persist state (a saved cookie jar, in my case) across separate Actor executions, open a named store instead - Actor.openKeyValueStore(name=...) in Python - and read/write through that. Obvious in hindsight; not obvious from the method names.
None of these are exotic. They're the kind of thing you find in 20 minutes with a good search - once you know the exact term to search for. Hopefully this saves the next person that 20 minutes, or the day I lost on the key-value store one. If you're building something similar (Playwright + Camoufox + Apify), happy to compare notes in the comments.
Comments
No comments yet. Start the discussion.