PRACTICAL GUIDE / Selenium Python remote file detector upload
Your upload test passes locally and silently uploads nothing on the Grid
A Selenium upload can fail without raising anything. How LocalFileDetector decides, where it gives up, and how to prove the file reached the browser node.
In this guide13 sections
- The transfer is conditional, and the condition is easy to miss
- What the transfer does when it does happen
- Worked example one: the relative path that resolves differently under pytest
- Worked example two: the assertion that cannot tell the difference
- Worked example three: the endpoint that is not there
- How to tell it is this and not a look-alike
- The second failure mode: multi-file uploads are all or nothing
- Wiring it into CI so the Grid path is exercised
- What it costs
- Rolling it out
- When not to do this
- FAQ
- Do I need to set the file detector in Python at all?
- Why does no exception appear when the upload does not happen?
- What happens when I upload several files at once?
- Can I mount a shared volume instead and skip the transfer?
- Is there a case for turning the detector off?
- How large a file can this handle?
- Practise the diagnosis
What you will learn
- The transfer is conditional, and the condition is easy to miss
- What the transfer does when it does happen
- Worked example one: the relative path that resolves differently under pytest
- Worked example two: the assertion that cannot tell the difference
The upload test is green on your laptop. It is green in the pre-merge check that runs local Chrome. Then it moves to the Grid job and the final assertion fails: the confirmation banner never appears, the attachments list stays empty, and the app under test is quietly reporting that no file was selected. There is no exception, no stack trace, and no error in the node log. send_keys returned normally.
What actually happened is that Selenium looked at your path, decided it was not a file, and typed it into the input as plain text. The browser, running on a different machine, received a string that means nothing on its filesystem, and did what browsers do with garbage in a file input: nothing.
The transfer is conditional, and the condition is easy to miss
Selenium's remote WebDriver documentation states the problem cleanly: "Uploading a file is more complicated for Remote WebDriver sessions because the file you want to upload is likely on the computer executing the code, but the driver on the remote computer is looking for the provided path on its local file system." The solution it names is the local file detector, which bundles the file and sends it to the remote machine.
The part that catches people is what happens when the bundling does not occur. Reading the Selenium 4.39.0 Python bindings, the flow inside WebElement.send_keys is:
- If the driver is not remote, do nothing special. Local drivers set
_is_remote = Falsein their constructors, so on local Chrome the path you pass goes straight through and works because the browser is on the same machine as the file. This is why the test is green on your laptop. - If the driver is remote, take the full value, join it, and split it on newline characters.
- Run the file detector over every resulting segment.
LocalFileDetector.is_local_filereturns the path whenPath(file_path).is_file()is true, and returnsNoneotherwise. Note the specific check:is_file(). A directory returnsNone. A path that does not exist returnsNone. A broken symlink returnsNone. - Transfer only if no segment came back
None. That is a single all-or-nothing gate over the whole list. - Whatever survives that gate is what gets sent to
send_keys.
Step four is the failure. If any one segment fails the detector, the transfer is skipped entirely and the original string is sent as keystrokes. Not an error, not a warning, not a log line. Just text.
The reason it is written that way is defensible: Selenium cannot distinguish "the user is uploading a file and got the path wrong" from "the user is typing a path-shaped string into a text field", and typing the string is the safer default. It is a reasonable design choice that produces a terrible debugging experience.
What the transfer does when it does happen
When all segments resolve, each file goes through _upload, and it is worth knowing the shape because two of the failure modes below are only legible if you do.
The client writes the file into an in-memory ZIP, using only the basename as the archive entry name, base64-encodes the archive, and issues the uploadFile command, which the Python remote connection maps to POST /session/<session-id>/se/file. The response value is a path string, and that path, not your local path, is what finally gets sent as keystrokes.
The receiving end is the Grid Node. In Selenium's LocalNode, the /session/{sessionId}/se/file route unzips the payload into a per-session temporary directory and returns the absolute path of the file it extracted. Three details from that implementation matter in practice:
- The node requires the archive to contain exactly one file. If it does not, it raises with
Expected there to be only 1 file. There were: N. The Python client only ever writes one entry per call, so you will normally see this only with a custom detector or a hand-rolled client. - When the session is running in a Docker container, the node forwards the upload command into the container rather than unpacking it itself, so the destination path is inside the container's filesystem.
- Uploaded files live in a cache keyed by session id, with a removal listener that deletes the temporary files and base directory when the session is evicted, plus a cleanup pass on a 30 second schedule. You do not need to clean them up, and you should not expect them to survive the session.
Because the returned remote path is what gets typed, the browser only ever sees a path that exists on its own machine. That is the entire mechanism. Everything that goes wrong is a variation on "the detector said no" or "the round trip did not happen".
Worked example one: the relative path that resolves differently under pytest
This is the most common version of the bug and it does not look like a Selenium problem at all.
upload = "fixtures/profile.png"
driver.find_element(By.CSS_SELECTOR, "input[type=file]").send_keys(upload)Run from the repository root, fixtures/profile.png exists, is_file() is true, the upload happens. Run from tests/, or under a CI step with a different working directory, or under a pytest invocation where rootdir is not what you assumed, and the relative path resolves to nothing. is_local_file returns None, the gate closes, and the literal string fixtures/profile.png is typed into the file input.
The fix is to make the path absolute and to refuse to proceed when it is not real. Anchoring on __file__ rather than the process working directory removes the entire class of problem:
"""Upload a fixture to a remote browser, and fail loudly if it cannot be sent."""
from pathlib import Path
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.file_detector import LocalFileDetector
# Anchor on this file, never on the process working directory.
FIXTURES = Path(__file__).resolve().parent / "fixtures"
def upload_fixture(driver, name: str, selector: str = "input[type=file]"):
path = (FIXTURES / name).resolve()
# Guard with exactly the predicate the detector uses. is_file() is False for
# directories and for broken symlinks, and either one silently disables the
# transfer without raising.
if not path.is_file():
raise FileNotFoundError(f"upload fixture is not a file: {path}")
# Redundant in Python 4.x, where Remote already assigns LocalFileDetector,
# but it documents the intent and survives a future refactor to a custom one.
driver.file_detector = LocalFileDetector()
element = driver.find_element(By.CSS_SELECTOR, selector)
element.send_keys(str(path))
return path
driver = webdriver.Remote(
command_executor="http://localhost:4444",
options=webdriver.ChromeOptions(),
)
try:
driver.get("https://example.test/profile")
upload_fixture(driver, "profile.png")
driver.find_element(By.ID, "submit").click()
finally:
driver.quit()Two things about that snippet are worth defending.
The is_file() guard is not belt-and-braces. It is the only place in the whole flow where a bad path can be turned into a loud failure, because Selenium deliberately will not do it for you. Ninety percent of the debugging time this article is about is spent because nobody wrote those two lines.
Setting driver.file_detector explicitly is functionally a no-op on modern Python Selenium. The Remote constructor assigns LocalFileDetector() when no detector is passed, and Selenium's own documentation says Python adds a local file detector to remote webdriver instances by default while Java does not. Keep the line anyway, as a note to the reader that this test depends on the detector; delete it if your team prefers less ceremony. What you must not do is copy the Java advice into a Python code review and claim the upload was broken because the line was missing.
Worked example two: the assertion that cannot tell the difference
The deeper problem is that most upload tests assert on something that is also true when the upload silently failed. A test that checks "no error banner is shown" passes perfectly against an empty file input.
The assertion has to be positive and specific, and there is a cheap trick that makes the transfer itself observable. Because the value sent to the browser is the node's temporary path rather than your local path, you can read the input's value property back and check that it does not contain your local directory:
"""Prove the transfer happened, not just that the page did not complain."""
from pathlib import Path
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.file_detector import LocalFileDetector, UselessFileDetector
def assert_transfer_occurred(driver, element, local_path: Path):
"""The browser must be holding a path produced by the node, not ours.
Browsers deliberately obscure the real path of a selected file, so we assert
on the basename surviving and on our own directory being absent, rather than
on an exact string.
"""
value = element.get_property("value") or ""
assert value, "file input is empty: the upload did not reach the browser"
assert local_path.name in value, f"unexpected input value: {value!r}"
assert str(local_path.parent) not in value, (
"the file input still holds the RUNNER path, so no transfer happened. "
"The detector returned None for at least one segment."
)
def detector_report(driver, *paths):
"""Ask the detector directly, before sending anything.
This is the fastest way to answer 'would this have uploaded?' without
launching a page or an assertion. None means the segment killed the
transfer for the whole send_keys call.
"""
detector = driver.file_detector
return {
str(p): detector.is_local_file(str(p)) is not None
for p in paths
}
# Example use during triage:
# >>> detector_report(driver, "fixtures/a.png", "/tmp/missing.png")
# {'fixtures/a.png': True, '/tmp/missing.png': False}
# Any False means send_keys will type text instead of uploading ANY of them.detector_report is the diagnostic to reach for first. It answers the only question that matters, it needs no page and no network, and it takes a second. is_local_file is a public method on the FileDetector interface, so calling it directly is fair game rather than a private-API hack.
The assert_transfer_occurred check needs one honesty caveat. Browsers intentionally do not expose real filesystem paths to page script, and Chromium-based browsers famously report C:\fakepath\name.ext for a selected file. So treat the basename assertion as the load-bearing one and the directory assertion as a bonus that works on some browsers and not others. If it proves flaky in your environment, drop it and keep the emptiness check, which is reliable everywhere.
Worked example three: the endpoint that is not there
There is a third failure that is worth knowing precisely because it is so quiet. Inside _upload, the Python client catches WebDriverException and, for three specific error signatures, returns the original local filename instead of re-raising. The signatures are a response containing Unrecognized command: POST, one containing Command not found: POST , and one containing the exact JSON fragment {"status":405,"value":["GET","HEAD","DELETE"]}.
In other words: when the remote end does not implement /se/file, the client falls back to sending your local path, without an error. That is a deliberate compatibility shim for endpoints that predate or do not implement the upload extension, and it is completely invisible from the outside.
You will meet this when webdriver.Remote points at something that is not a Selenium Grid Node. Appium servers, some cloud provider endpoints, custom WebDriver implementations, and old standalone servers all sit in this category to varying degrees. The symptom is identical to the detector returning None, which is why the diagnostic above is worth running first: if detector_report says True for every path and the upload still did not land, the endpoint is your suspect, not the detector.
Confirming it takes one request against a live session:
# 1. Is the Grid up, and which nodes are registered?
curl -s http://localhost:4444/status | jq '.value.ready, (.value.nodes | length)'
# 2. Does this endpoint implement the upload route at all?
# A Grid Node routes POST /session/<id>/se/file. Anything that answers with
# "Unrecognized command", "Command not found", or a 405 listing only
# GET/HEAD/DELETE will make the Python client fall back SILENTLY.
SESSION_ID="paste-a-live-session-id-here"
curl -s -o /dev/null -w '%{http_code}\n' \
-X POST -H 'Content-Type: application/json' \
--data '{"file":"not-a-real-zip"}' \
"http://localhost:4444/session/${SESSION_ID}/se/file"
# 3. What did the node actually unpack? Ask the node's own log, not the client.
# Run the Grid with a verbose level to see per-command handling.
# java -jar selenium-server-<version>.jar standalone --log-level FINE
docker logs selenium-node-chrome 2>&1 | grep -i "se/file\|upload"A malformed body will not succeed, and it is not meant to. What you are reading is the status code and the error shape: a route that exists rejects your payload one way, a route that does not exist rejects the request an entirely different way, and that difference is the answer.
How to tell it is this and not a look-alike
Four other failures produce "the upload did not work", and each has a distinguishing signal.
An exception was raised. If you got ElementNotInteractableException, this article is the wrong one. That means Selenium found the element and refused to interact with it, typically because the input[type=file] is hidden behind a styled label, which is the standard pattern in modern component libraries. The W3C strictFileInteractability capability governs how strictly the driver enforces visibility for file inputs specifically, and Selenium exposes it as the strict_file_interactability option. The file detector never entered the picture.
You located the label, not the input. A .upload-button selector that matches a <div> or <label> will accept send_keys on some drivers and do nothing useful. Confirm with element.get_attribute("type") == "file" before blaming the transfer. This one is common enough that it is worth putting into the helper.
The transfer worked and the app rejected the file. If the input's value is populated and the basename is right, the file crossed the wire successfully and the failure is downstream: MIME type validation, a size cap, an antivirus scan, a signed-URL step. The evidence is a populated input plus a specific server-side message, and the fix is in the application, not the test.
The path resolves but points at the wrong thing. A directory passes a naive existence check and fails is_file(). So does a symlink whose target was cleaned up between test runs. detector_report catches both in one call.
The clean discriminator, stated once: this problem produces no exception and an empty or literal-path file input. Any other combination points elsewhere.
The second failure mode: multi-file uploads are all or nothing
Selenium supports multi-file uploads by newline-separating the paths, and the Python implementation uploads each one individually before joining the returned remote paths back together with newlines. That works well, right up until one path is wrong.
Because the gate is if None not in local_files, a single bad entry in a list of five disables the transfer for all five. You do not get four files uploaded and one missing. You get zero files uploaded, and a file input containing a newline-joined blob of local paths as text. Teams debugging this usually assume a partial failure and spend an hour looking for which one file went missing.
The guard is straightforward once you know the rule, and it belongs in the helper rather than in each test:
def upload_many(driver, element, paths):
"""Upload several files, failing loudly instead of silently sending none.
Selenium joins paths with newlines and transfers ONLY if every segment
resolves to an existing file. One bad path disables the whole upload,
so validate the whole list up front.
"""
resolved = [Path(p).resolve() for p in paths]
bad = [str(p) for p in resolved if not p.is_file()]
if bad:
raise FileNotFoundError(
"these paths are not files, so NONE of the uploads would be sent: "
+ ", ".join(bad)
)
element.send_keys("\n".join(str(p) for p in resolved))
return resolvedOne further constraint worth stating: the element has to actually accept multiple files. If the page's input lacks the multiple attribute, the browser takes the first path and discards the rest, and that is browser behaviour rather than anything Selenium controls. Assert on element.get_attribute("multiple") if your test depends on it.
Wiring it into CI so the Grid path is exercised
The reason this bug reaches production suites is that the local job and the Grid job are not the same job. If the only place uploads run remotely is the nightly full suite, you will find out about a regression twelve hours late.
# docker-compose.yml
# A two-container Grid that the upload suite can run against on every PR.
services:
selenium-hub:
image: selenium/hub:4.39.0 # pin the version, never use :latest
container_name: selenium-hub
ports:
- "4442:4442" # event bus publish
- "4443:4443" # event bus subscribe
- "4444:4444" # router / client entry point
environment:
# Fail fast instead of parking a request in the queue for five minutes
# when nothing on the Grid can serve it.
- SE_SESSION_REQUEST_TIMEOUT=120
chrome:
image: selenium/node-chrome:4.39.0
depends_on:
- selenium-hub
shm_size: 2gb # Chrome crashes in the default 64m /dev/shm
environment:
- SE_EVENT_BUS_HOST=selenium-hub
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
- SE_NODE_MAX_SESSIONS=2
- SE_NODE_OVERRIDE_MAX_SESSIONS=trueDeliberately, there is no volumes: entry mapping a fixtures directory into the node. Mounting the fixtures would make the upload tests pass without exercising the file detector at all, which defeats the purpose of running them here. The whole value of the Grid job is that it is the one place where a broken transfer is visible.
The pytest side wires the remote driver and makes the mode explicit in the test report:
# conftest.py
"""Run the same upload tests locally and against a Grid, and say which."""
import os
import pytest
from selenium import webdriver
from selenium.webdriver.remote.file_detector import LocalFileDetector
GRID_URL = os.environ.get("SELENIUM_REMOTE_URL")
@pytest.fixture
def driver(request):
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
if GRID_URL:
drv = webdriver.Remote(command_executor=GRID_URL, options=options)
# Explicit for the reader. Remote already defaults to LocalFileDetector.
drv.file_detector = LocalFileDetector()
else:
drv = webdriver.Chrome(options=options)
# _is_remote is what actually decides whether send_keys transfers files.
# Recording it puts the mode in the report instead of in someone's head.
request.node.add_report_section(
"call", "selenium-mode", f"remote={drv._is_remote} url={GRID_URL or 'local'}"
)
yield drv
drv.quit()
@pytest.fixture
def require_remote(driver):
"""Skip transfer-specific assertions when running against local Chrome."""
if not driver._is_remote:
pytest.skip("file transfer only occurs for remote sessions")
return driverThe require_remote fixture matters more than it looks. Without it, the transfer assertions from worked example two fail on local Chrome for the entirely correct reason that no transfer was supposed to happen, and someone will "fix" that by deleting the assertion.
What it costs
The engineering is small. The costs are real but bounded.
Every upload gets slower. The file is zipped, base64-encoded, sent as a JSON body, and unzipped on the node. Base64 inflates payload size by roughly a third before compression is accounted for, and both ends hold the encoded string in memory. For a 40 KB avatar this is noise. For a 200 MB video it is not, and you will feel it in both wall-clock time and node memory.
The guards add failures you did not have. A FileNotFoundError on a missing fixture is a new way for the suite to go red. That is the point, but it will fire on someone's branch during a rename and it will look like the change broke things. It did not; it made an existing breakage visible.
Absolute paths hide platform bugs. Anchoring on __file__ and calling .resolve() fixes the working-directory problem so thoroughly that you stop noticing path handling entirely. That is fine until a Windows contributor hits a case-sensitivity or separator issue that the helper was papering over. Run the suite on both platforms occasionally.
The Grid job costs CI minutes. Two containers, a session queue, and a network hop per test. Scope it to the upload and download specs rather than running the whole suite twice.
Rolling it out
Start with detector_report on the failing test. One call, one second, and it tells you whether you are debugging a path problem or an endpoint problem. Do not change any code before this.
Fix the paths before touching anything else. Most teams find the root cause here and stop. Anchor on __file__, resolve, and guard.
Then make the assertions positive. Replace "no error appeared" with "the input holds a file whose basename matches". This is the change that stops the bug recurring silently, and it is worth doing even in suites that currently pass.
Add the Grid job last. It is the most expensive step and the least urgent, because the guards will already have caught the common cases. Add it when you want the transfer path itself under continuous test.
For adjacent ground, remote file upload with the file detector on Selenium Grid covers the Grid-side view, file upload and download verification in Java covers the language where the detector genuinely is not set for you, and driver fixtures for parallel pytest runs covers the ownership problems that appear once these fixtures run concurrently.
When not to do this
When you are typing a path on purpose. This is the case the API was designed for and it is the one people never see coming. A test that fills a text field with /etc/hosts, or a search box with a filename that happens to exist on the runner, will have that string silently replaced by a temporary path on the node. Selenium ships UselessFileDetector, which always returns None, and a context manager to scope it:
from selenium.webdriver.remote.file_detector import UselessFileDetector
with driver.file_detector_context(UselessFileDetector):
search_box.send_keys("/etc/hosts") # typed as text, never uploadedfile_detector_context restores the previous detector when the block exits, so use it around the single interaction rather than swapping the detector for the whole session and forgetting to swap it back.
When the file is genuinely large. Above some threshold that depends on your node memory and network, the zip-and-base64 round trip stops being a reasonable way to move bytes. Seed the fixture through the application's own API or object store, and test the upload widget separately with a small file. You lose nothing: the widget behaviour and the large-file pipeline are different risks and deserve different tests.
When there is no file input to send keys to. Drag-and-drop zones built on the DataTransfer API, native OS file pickers opened by a button, and camera or clipboard capture flows do not expose an input[type=file] for Selenium to target. No file detector can help, because there is nothing to send keys to. These need either a test hook in the application that reveals a real input, or a different tool.
When your architecture makes the mount the honest answer. If your browsers and your test runner genuinely always share a filesystem, and always will, a mounted fixture directory is simpler and faster than the transfer. Make that a recorded decision rather than an accident, write down that the suite now requires colocated browsers, and accept that pointing it at a cloud Grid later is a migration rather than a config change. The failure mode of the accidental version is that someone points it at a cloud Grid on a Friday and spends the weekend on it.
When the upload is not the risk you are testing. Plenty of tests need a file present to reach the screen they actually care about. Those should seed the file through the fastest path available, usually the API, and spend their assertions on the thing under test. Routing every fixture through the browser upload widget makes the whole suite slower and couples unrelated tests to this mechanism.
FAQ
Do I need to set the file detector in Python at all?
Usually not, and that is exactly why the failure is confusing. In Selenium 4 the Python Remote constructor assigns LocalFileDetector when you do not pass one, and the Selenium docs state that Python adds a local file detector to remote webdriver instances by default. Java is the opposite: the same docs say Java does not include one by default, so a Java suite must call setFileDetector explicitly. Setting it in Python is documentation for the next reader rather than a functional change.
Why does no exception appear when the upload does not happen?
Because "this string is not a file" is a valid outcome, not an error. LocalFileDetector returns None for anything that is not an existing file, and send_keys reads that as "the caller wanted to type text", so it types the string into the input and returns normally. The browser then sees a path that means nothing on its machine, and the only evidence is a failed assertion further down the test.
What happens when I upload several files at once?
Selenium splits the value on newlines, runs the detector over every segment, and only transfers when all of them resolve. If one path in a five-file upload is wrong, the other four are not uploaded either and the entire newline-joined string is typed as text. That all-or-nothing rule is the single most confusing thing about multi-file uploads and it is worth asserting against explicitly.
Can I mount a shared volume instead and skip the transfer?
It works, and it costs you portability. A mounted path makes the test depend on the filesystem layout of whichever machine runs the browser, so the same test stops working the moment it is pointed at a cloud Grid, a different container image, or a colleague's laptop. Choose it deliberately for genuinely large files, not as a shortcut around a failing detector.
Is there a case for turning the detector off?
Yes, and it is the reason UselessFileDetector exists. If a test types a path-shaped string into an ordinary text field, and that string happens to name a real file on the runner, the detector will upload it and replace what you typed with a temp path on the node. Wrapping that one interaction in file_detector_context(UselessFileDetector) suppresses the behaviour for the smallest possible scope.
How large a file can this handle?
There is no documented limit, but the mechanism sets a practical one. The client zips the file, base64-encodes it, and sends the result inside a single JSON request body, which inflates the payload and holds it in memory on both ends at once. Small fixtures are fine. Multi-hundred-megabyte media files are better handled by seeding the object store through an API and testing the upload widget separately.
Practise the diagnosis
Take this into the QABattle arena. Given a passing local run, a failing Grid run, and no exception anywhere, what is the first command you run and what does each of its two possible answers rule out? If you can answer that in one breath, this bug will never cost you more than a minute again.
// FIELD DISPATCH
Get the QA Field Notes
Weekly QA battles, AI testing guides, and interview drills. Free on Substack.
// LIVE COURSE / THE TESTING ACADEMY
Playwright Automation Mastery
Go beyond Selenium. Master Playwright with JS/TS in 90 days.
From the instructor behind this guide.
Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.
PRIMARY REFERENCES
Verify the details at the source
QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.
- 01Official selenium.dev reference
selenium.dev
Primary documentation selected and verified for the claims in this guide.
- 02Official selenium.dev reference
selenium.dev
Primary documentation selected and verified for the claims in this guide.
- 03Official selenium.dev reference
selenium.dev
Primary documentation selected and verified for the claims in this guide.
- 04Official w3.org reference
w3.org
Primary documentation selected and verified for the claims in this guide.
FAQ / QUICK ANSWERS
Questions testers ask
Do I need to set the file detector in Python at all?
Usually not, and that is exactly why the failure is confusing. In Selenium 4 the Python Remote constructor assigns LocalFileDetector when you do not pass one, and the Selenium docs state that Python adds a local file detector to remote webdriver instances by default. Java is the opposite: the same docs say Java does not include one by default, so a Java suite must call setFileDetector explicitly. Setting it in Python is documentation for the next reader rather than a functional change.
Why does no exception appear when the upload does not happen?
Because 'this string is not a file' is a valid outcome, not an error. LocalFileDetector returns None for anything that is not an existing file, and send_keys reads that as 'the caller wanted to type text', so it types the string into the input and returns normally. The browser then sees a path that means nothing on its machine, and the only evidence is a failed assertion further down the test.
What happens when I upload several files at once?
Selenium splits the value on newlines, runs the detector over every segment, and only transfers when all of them resolve. If one path in a five-file upload is wrong, the other four are not uploaded either and the entire newline-joined string is typed as text. That all-or-nothing rule is the single most confusing thing about multi-file uploads and it is worth asserting against explicitly.
Can I mount a shared volume instead and skip the transfer?
It works, and it costs you portability. A mounted path makes the test depend on the filesystem layout of whichever machine runs the browser, so the same test stops working the moment it is pointed at a cloud Grid, a different container image, or a colleague's laptop. Choose it deliberately for genuinely large files, not as a shortcut around a failing detector.
Is there a case for turning the detector off?
Yes, and it is the reason UselessFileDetector exists. If a test types a path-shaped string into an ordinary text field, and that string happens to name a real file on the runner, the detector will upload it and replace what you typed with a temp path on the node. Wrapping that one interaction in file_detector_context(UselessFileDetector) suppresses the behaviour for the smallest possible scope.
How large a file can this handle?
There is no documented limit, but the mechanism sets a practical one. The client zips the file, base64-encodes it, and sends the result inside a single JSON request body, which inflates the payload and holds it in memory on both ends at once. Small fixtures are fine. Multi-hundred-megabyte media files are better handled by seeding the object store through an API and testing the upload widget separately.
RELATED GUIDES
Continue the learning route
GUIDE 01
Remote File Uploads with LocalFileDetector on Selenium Grid
Use Selenium LocalFileDetector upload correctly on Grid, trace local-to-node file transfer, and diagnose path, input, and server failures.
GUIDE 02
Selenium Java File Upload and Download Verification
Selenium Java file upload download testing: practical design, implementation, debugging, CI, metrics, and interview guidance for QA, SDET, and automation engineers.
GUIDE 03
Selenium Python Tutorial: Build Your First Browser Test
Selenium Python tutorial for beginners covering setup, locators, waits, pytest fixtures, page objects, debugging, CI, and stable browser tests.
GUIDE 04
Session-Safe Selenium Python Fixtures for Parallel pytest Runs
Design function-scoped Selenium Python fixtures for pytest-xdist with isolated drivers, collision-free artifacts, failure capture, and reliable cleanup.
GUIDE 05
Route External WebDriver Sessions Through a Selenium Grid Relay Node
Configure a Selenium Grid relay node to route matched sessions to an external WebDriver service with explicit capacity, health checks, and failure controls.