skip to content

A Selenium upload passes locally but the remote browser cannot find the file - why, and what fixes it?

level: seniorimportance: should knowfreq 46%

answer

  1. Two machines, two filesystems
  2. The default detector finds nothing
  3. Something has to carry the bytes across
  4. Set it on the driver, not the element
  5. LocalFileDetector via setFileDetector

basics

~20 s

Java's RemoteWebDriver starts with a UselessFileDetector, so sendKeys forwards your local path as plain text and the browser's own machine has no such file. Call setFileDetector with a LocalFileDetector so the client ships the file first.

solid answer

~40 s

Locally the runner and the browser share a disk, so an absolute path just works. Against a remote session the browser has its own filesystem, and Selenium 4's Java `RemoteWebDriver` defaults to a `UselessFileDetector` whose `getLocalFile` always returns `null` - so the path is forwarded verbatim, the remote end's existence check fails, and you get `invalid argument`. The fix is one line: `((RemoteWebDriver) driver).setFileDetector(new LocalFileDetector())`. With that installed, `sendKeys` splits its text on newlines, resolves each segment against the client's disk, zips and base64-encodes each resolved file, posts it to `POST /session/{sessionId}/se/file`, and sends the remote end's returned path instead of yours. Set it once at session creation, before any element is found.

code

java · 23 lines
java
import java.net.URL;
import java.nio.file.Path;
import org.openqa.selenium.By;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.remote.LocalFileDetector;
import org.openqa.selenium.remote.RemoteWebDriver;

public class RemoteLeafUpload {

  public static void main(String[] args) throws Exception {
    URL gridUrl = new URL("http://grid.internal:4444");
    RemoteWebDriver driver = new RemoteWebDriver(gridUrl, new ChromeOptions());
    driver.setFileDetector(new LocalFileDetector());
    try {
      driver.get("https://garden-centre.example/plant-finder");
      Path photo = Path.of("src/test/resources/fern-leaf.jpg").toAbsolutePath();
      driver.findElement(By.id("leaf-photo")).sendKeys(photo.toString());
      driver.findElement(By.id("identify-plant")).click();
    } finally {
      driver.quit();
    }
  }
}

go deeper

for a junior

Recall that a remote browser runs on a different machine and cannot see your files, and that Selenium has a file detector for exactly this. Naming LocalFileDetector is enough here.

for a middle

Explain the default UselessFileDetector, what setFileDetector changes, and that the client zips the file and posts it before the send-keys command so the browser receives a path on its own filesystem.

for a senior

Diagnose it from symptoms: the same test green locally and invalid argument remotely. Know the all-or-nothing resolution rule, that only regular files resolve, and that the detector is per driver and copied onto elements as they are found.

for a principal

Own where the setting lives and what it costs. Decide whether uploads ship over HTTP on every run or the fixture is pre-placed beside the browser, and make sure the choice is visible at session creation rather than buried in tests.

## The default is to do nothing `RemoteWebDriver` holds a `FileDetector`, and its initial value is a `UselessFileDetector` - a class whose `getLocalFile` is written to return `null` every single time. With that in place, `sendKeys` treats a path exactly as it treats any other string: it posts the characters to the remote end and nothing else happens. That is fine when the browser is on your machine, because your test runner and the browser read the same disk. The moment the session is a **remote** one - a Grid node on another host, a browser inside a container, a relayed endpoint - the string `/home/ci/fixtures/fern-leaf.jpg` is a promise about a filesystem the browser cannot see. The remote end dutifully verifies that the named file exists, finds that it does not, and returns `invalid argument`. ## What LocalFileDetector adds `LocalFileDetector` implements the same one-method interface, but resolves the characters against the **client's** disk: it concatenates the `CharSequence` arguments, treats the result as a path, and returns the `File` only if it exists and is a regular file. Install it and the client does real work before typing: 1. `RemoteWebElement.sendKeys` joins its arguments and splits the result on the newline character. 2. Each segment is handed to the detector; a segment that names an existing local file resolves to a `File`. 3. If **every** segment resolved, each file is zipped and base64-encoded, and posted to `POST /session/{sessionId}/se/file` - a Selenium extension endpoint, not a W3C command. 4. The remote end unzips the payload into a temporary directory it owns and returns the **absolute path of the unzipped file on its own filesystem**. 5. Those returned paths, joined by newlines, become the text of the ordinary Element Send Keys call. So the string the browser finally sees is never the path you wrote. It is a path the remote end minted, pointing at a copy of your file that now genuinely exists next to the browser. ## The two detectors side by side | | `UselessFileDetector` (default) | `LocalFileDetector` | |---|---|---| | `getLocalFile` returns | always `null` | the `File`, when it exists and is a regular file | | Extra HTTP command | none | one `POST .../se/file` per resolved path | | Text sent to the input | your original path | the remote end's path to its unzipped copy | | Correct when | the browser shares your disk | the browser has its own filesystem | ## Installing it `setFileDetector` lives on `RemoteWebDriver`, so the cast is the usual one when your field is typed as `WebDriver`. The setting is per driver instance and is copied onto each `RemoteWebElement` as elements are created, so set it once, right after the session is created, before any element is found. ## Failure modes worth knowing - **All or nothing.** If any newline-separated segment fails to resolve to a local file, the client uploads *none* of them and sends the raw string. One typo in a two-file list silently degrades the whole call into plain text. - **Directories are ignored.** The detector demands a regular file, so a directory path resolves to `null` and takes the whole call down the no-upload path with it. - **Ordinary typing can be captured.** With the detector installed, any `sendKeys` string that happens to name an existing file is uploaded. Typing a plant name that matches a file in the process's working directory is the pathological case; it is rare, but it is why the default is the do-nothing detector. - **A node that does not share the browser's filesystem forwards the upload.** When the browser runs in a container or a pod rather than beside the node process, the node passes the upload command on to that environment instead of writing to its own disk - otherwise the unzipped file would land somewhere the browser cannot read. ## The garden-centre shape A plant-finder test that uploads `fern-leaf.jpg` reads identically in both environments; the only difference is one line at session setup: ```java WebDriver driver = new RemoteWebDriver(gridUrl, new ChromeOptions()); ((RemoteWebDriver) driver).setFileDetector(new LocalFileDetector()); driver.findElement(By.id("leaf-photo")).sendKeys(photo.toAbsolutePath().toString()); ``` Note that this default is a **Java-binding** fact rather than a protocol one: the Python client's remote `WebDriver` constructor already falls back to a `LocalFileDetector` when none is supplied, and exposes a `file_detector` attribute you assign instead of a setter. The wire behaviour is identical in either language, because the zip-and-post step lives in the client, not in the browser - only the default differs.

  • What happens if one of several newline-separated paths does not exist locally?
    None of them are uploaded. The client only takes the upload path when every segment resolves to an existing local file; if any resolves to null, it falls back to sending the raw string. The result is a confusing invalid argument from the remote end rather than a message naming the missing fixture, so validate fixtures before calling sendKeys.
  • Is there any risk in leaving LocalFileDetector installed for all sends?
    A small one. Every sendKeys string is now checked against the local disk, so text that happens to name an existing file gets uploaded instead of typed. It is rare in practice, but it is why the default detector does nothing, and it is a reason to keep the setting on remote sessions rather than applying it universally.
  • Is the do-nothing default the same in every language binding?
    No, and it catches people moving between them. Java's RemoteWebDriver starts with a UselessFileDetector and needs setFileDetector before a remote upload works. The Python client's remote WebDriver constructor falls back to a LocalFileDetector when none is passed, so the same test needs no extra line there. The protocol behaviour is identical; only the client default differs.

The default detector posts the browser a note that says the photo is in your desk drawer - perfectly true, and useless to anyone sitting at a different desk. LocalFileDetector is the courier that goes ahead with the photo itself and comes back with the address it was filed under.

saying these in an interview costs you the question

  • Assuming the remote browser can read the test machine's disk
  • Setting the detector on the element rather than the driver
  • Blaming the path format when the file was never transferred
  • Believing the original local path is what reaches the browser
  • Expecting the detector to upload a directory of fixtures