Skip to content

Build1 publisher2 min readPublished

DOM.setFileInputFiles returns success while the input still holds zero files

A developer driving a real logged-in Chrome over raw DevTools Protocol logged five failures that never raised an exception, and the only thing that caught the failed upload was reading the file input back in the page.

The Engineer · Build desk

Illustration accompanying DOM.setFileInputFiles returns success while the input still holds zero files

What happened

  • The setup is a real logged-in Chrome on a persistent user-data-dir with --remote-debugging-port=9243, driven from Python over raw DevTools Protocol with websocket-client and no Selenium or Playwright.
  • Chrome blocked the first automated Google sign-in popup because the click was not a user gesture it recognised, and it failed silently.
  • Headed Chrome routes Input.* events only to the foreground tab, so a backgrounded tab silently drops clicks, keys and insertText.
  • DOM.setFileInputFiles reported success on a Gumroad zip upload while the page's file input still reported files.length of 0.
  • React drop-zones ignore the file input entirely, listening for dragenter, dragover and drop and reading the files from event.dataTransfer.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • decision With no exception raised, verification moves into the caller: every write needs a read-back assertion in the page, or the run records a success it never had.
  • constraint Foreground ownership has to be re-asserted before each input call, so step ordering becomes part of correctness for any script that interleaves tabs.
  • exposure On a profile a person also uses, matching a target by URL substring can put the script inside a stranger's live OAuth flow and grant consent with real credentials.
  • cost The flag that unblocks the sign-in, --disable-popup-blocking, weakens the same profile used for ordinary browsing, and the human sharing that browser pays for it.

Fifteen identical clicks landed nowhere before the cause was clear. The coordinates were right, and elementFromPoint at those coordinates returned the intended button [11]. Two calls fixed it: Target.activateTarget with the tab's own targetId, then Page.bringToFront [13]. After that, the same click sequence completed the whole OAuth flow in two steps [14]. The click payload was identical across the failures and the success [20]. Activation also has to be repeated per step: the author re-claims the foreground between steps in case something else takes it back [13].

The upload call reports success either way, so the check has to be a property read in the page after the call [15]. The author's explanation for the empty input is that DOM.setFileInputFiles is a no-op unless the backend node actually resolves to a real file input [16]. The fallback most people reach for next is unavailable on this box: on a bare X display with no window manager, clicking the input to drive the native dialog opens nothing for xdotool to type into [17]. Faking it with a synthetic change event on a hidden input does not work [18]. The payload that worked carries one item with mimeType application/zip and the zip base64-encoded, with dragOperationsMask set to 1 [19].

On a profile a human also uses, picking the wrong target lets the script act outside its own job. The intended flow is to click the provider button, wait for a new target whose URL starts with accounts.google.com, and attach to that target's websocket [6]. Picking the first matching target instead attached the script to a different tab's Luma OAuth flow, and it clicked through that consent screen [9]. The fix is to diff the target list: snapshot target ids before the click, take the id that appears afterwards, and sanity-check openerId against your own tab [10]. Same-tab OAuth needs the other half of the poll. On dev.to, "Continue with Google" navigated the current tab to accounts.google.com and back to a /users/auth/google_oauth2/callback URL, so code watching only for a new target sits there forever [7]. Poll for both [8].

This is one headed Chrome on one machine with one persistent profile, driven from Python over websocket-client against /json/list [1][2]. The post does not state the Chrome version [21]. Popup gesture recognition, Input.* routing and node resolution inside setFileInputFiles are all internals that can move between releases. After every write, read the property the page itself reads. The author wrote that the setup "fails in ways that never show up as exceptions, which is the part worth writing down" [3].

What to watch

  • A Chrome release that changes Input.* delivery or popup gesture recognition would invalidate the activate-then-click fix.
  • The published text stops mid drag-dispatch loop, so the exact dragenter/dragover/drop ordering and coordinates are not shown.
  • Whether running a minimal window manager on the X display restores the native file dialog as a usable fallback.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories