DEV Community

My automation read another site's page: the active tab belongs to the browser, not to your session

One of my scheduled jobs was working on LinkedIn. It called into the page and read back a Reddit thread. No exception, no timeout, no empty result - the call returned a document, a title, and HTML that parsed fine. It was just somebody else's page. I run several scheduled jobs that drive one real Chrome over the DevTools Protocol, each with its own named client session. When two of them overlap, one reads the other's page and has no way to tell that it did. The diagnosis that cost me the most time I went looking for a stray navigation in my own code first - a leftover open from an earlier step, or a redirect I had not accounted for. Then I went looking for a second tab, because "my job ended up on another domain" reads like something opened a tab. There is no second tab. That is the detail that keeps you searching in the wrong place, so it is worth showing before the mechanism. Expected, observed, negative control Throwaway profile on its own debugging port, Chrome 152.0.7977.76, driver invoked as a fresh process per command: $ tab list โ†’ [t1] about:blank # job A $ --session jobA open https://example.com/ $ --session jobA eval "location.href" "https://example.com/" # job B, a different session, doing its own unrelated work $ --session jobB open https://example.org/ # job A again. It has issued no navigation of its own since the read above. $ --session jobA eval "location.href" "https://example.org/" $ --session jobA eval "document.title" "Example Domain" $ tab list โ†’ [t1] example.org - https://example.org/ One tab for the whole sequence. Job B did not open anything; it steered the tab job A was standing on. And job A's read succeeds - it gets a location and a title back, so every downstream "did the page load" check answers yes. The fix that looks obvious and does not work Give each job its own tab. I measured that before shipping it, mostly out of habit: $ --session jobB tab new $ --session jobB open https://example.net/ $ tab list [t1] example.org - https://example.org/ โ†’ [t2] example.net - https://example.net/ $ --session jobA eval "location.href" "https://example.net/" Two tabs, one per job, each sitting at the URL its own job put there. Job A's tab is still on example.org, untouched. Job A reads example.net anyway. Where the behaviour actually lives The read does not resolve to "the tab this session created". It resolves to whichever tab is active in the browser, and active is a property of the browser, not of the client session. --session names a client; it does not name a target. Every command that says the page means the page the browser currently has in front, and the last job to navigate anything wins that for everyone. Negative control, same run, immediately after the failure above: $ --session jobA tab t1 $ --session jobA eval "location.href" "https://example.org/" $ --session jobB tab t2 $ --session jobB eval "location.href" "https://example.net/" Symmetric, and the failure goes away the moment each job re-asserts the cursor. The variable is the cursor, not the session and not the tab. Two ways out, and they cost different things Re-assert the tab immediately before every read. It works and it is cheap, but it has to be every single command, forever. Miss one and you are back to a read that returns valid data from the wrong page, which is the one failure mode nothing downstream can catch for you. Or serialise: an exclusive lock keyed by the debugging port, taken for the whole job and released on exit. One job drives the browser at a time. You pay in wall clock, since the browser sits idle while whoever holds the lock is thinking, and you have to reclaim the lock from a crashed holder or the first crash blocks every job after it. I use this, plus the per-read switch on top of it, because the two failures are independent - the lock protects me from another job, the switch protects me from my own earlier step. What this covers and what it does not Measured on macOS with Chrome 152.0.7977.76, one browser on one port, the driver invoked as a fresh process per command. It should not apply to a client that holds a page or target handle for the life of the session and addresses its commands to that handle. This failure needs a driver that resolves the page from browser state at command time. I have not measured another client to see which of those two shapes it has, and that is the thing worth checking in your own stack, because the difference is not visible in the API surface - it is in what the driver keeps between two commands. Separate ports are a different question and I did not run it. My lock is keyed by port on the assumption that two browsers on two ports cannot take each other's cursor. That follows from the cursor being per-browser, but following from something is not the same as having measured it. Top comments (0)

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.