Repository navigation
Description
Summary
When a file delivery fails in a streamed preview, the upload reports that the page's file picker is no longer open even though that picker remains answerable. The upload should report the delivery failure and keep the visible picker state consistent with the server. A focused service test reproduced the mismatch by making the first delivery fail and retrying the same chooser successfully; web and mobile clear their picker prompts before receiving the error.
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/server
Steps to reproduce
- From the checkout at the version below, run the script with the checkout path as its sole argument. It copies the existing preview browser test harness to a uniquely named temporary test file, adds one case with a rejecting first
setFilescall and a successful second call, and runs only that case. - The test records the first answer after
setFilesrejects. If that answer isfalse, it retries the same chooser ID; otherwise it opens a new chooser. It checks that the second delivery succeeds and thatsetFileswas called twice. The successful second delivery is the control.
The final script reproduced on its one run; an earlier script version also reproduced. The first experimental script run was inconclusive because its test passed but its output check expected a console line suppressed by the test runner. A real page or transport failure that makes Playwright reject setFiles was not exercised.
Save the following as repro.sh and run sh repro.sh /path/to/t3code from any directory. It requires Node, the checkout's installed dependencies, and vp; it prints the tool versions, puts HOME, TMPDIR, and T3CODE_HOME under a fresh temporary directory, and removes the temporary test and directory on exit. Exit 1 and RESULT: defect reproduced mean the first answer was false but the same chooser remained usable. Exit 0 and RESULT: expected behavior mean the delivery error surfaced as an error while a new chooser worked. Exit 2 and RESULT: inconclusive mean setup or the control failed.
Reproduction script: repro.sh
#!/bin/sh
set -u
if [ "$#" -ne 1 ]; then
printf 'RESULT: inconclusive\n'
exit 2
fi
repo=$1
scratch=$(mktemp -d)
test_file="$repo/apps/server/src/preview/ServerBrowser.x04-03.test.ts"
cleanup() {
rm -f "$test_file"
rm -rf "$scratch"
}
trap cleanup EXIT HUP INT TERM
export HOME="$scratch/home" TMPDIR="$scratch/tmp" T3CODE_HOME="$scratch/t3"
export X04_03_PROBE_OUT="$scratch/probe.txt"
mkdir -p "$HOME" "$TMPDIR" "$T3CODE_HOME"
if ! command -v vp >/dev/null 2>&1 || ! command -v node >/dev/null 2>&1; then
printf 'RESULT: inconclusive\n'
exit 2
fi
node --version
vp --version
if [ ! -f "$repo/apps/server/src/preview/ServerBrowser.test.ts" ] || [ -e "$test_file" ]; then
printf 'RESULT: inconclusive\n'
exit 2
fi
cp "$repo/apps/server/src/preview/ServerBrowser.test.ts" "$test_file"
cat >> "$test_file" <<'TEST'
it.live("x04-03 a rejected page file delivery is reported as a stale chooser", () =>
Effect.scoped(
Effect.gen(function* () {
const { browser, tabId } = yield* ready;
const viewer = yield* browser.attachViewer(viewerInput(tabId, true));
yield* viewer.input({ type: "takeControl" });
const setFiles = vi.fn()
.mockRejectedValueOnce(new Error("file delivery failed"))
.mockResolvedValueOnce(undefined);
contexts[0]!.page.emit("filechooser", {
isMultiple: () => false,
element: () => fileChooserElement(".txt"),
setFiles,
});
let offered = yield* Queue.take(viewer.output);
while (offered._tag !== "fileChooser") offered = yield* Queue.take(viewer.output);
const input = {
threadId: scope.thread.threadId,
tabId,
chooserId: offered.id,
files: [{ name: "a.txt", mimeType: "text/plain", buffer: Buffer.from("a") }],
};
const firstExit = yield* Effect.exit(browser.answerFileChooser(input));
const first = Exit.isSuccess(firstExit) ? String(firstExit.value) : "error";
let second: boolean;
if (first === "false") {
second = yield* browser.answerFileChooser(input);
} else {
contexts[0]!.page.emit("filechooser", {
isMultiple: () => false,
element: () => fileChooserElement(".txt"),
setFiles,
});
let next = yield* Queue.take(viewer.output);
while (next._tag !== "fileChooser") next = yield* Queue.take(viewer.output);
second = yield* browser.answerFileChooser({ ...input, chooserId: next.id });
}
yield* Effect.promise(() => NodeFSP.writeFile(
process.env.X04_03_PROBE_OUT!,
`first=${first} second=${second} attempts=${setFiles.mock.calls.length}`,
));
expect(second).toBe(true);
expect(setFiles).toHaveBeenCalledTimes(2);
}),
).pipe(Effect.provide(layer)),
);
TEST
cd "$repo" || { printf 'RESULT: inconclusive\n'; exit 2; }
vp test run apps/server/src/preview/ServerBrowser.x04-03.test.ts -t 'x04-03 a rejected page file delivery is reported as a stale chooser' > "$scratch/test.log" 2>&1
status=$?
cat "$scratch/test.log"
if [ "$status" -eq 0 ] && grep -Eq 'Tests[[:space:]]+1 passed' "$scratch/test.log" && [ -f "$X04_03_PROBE_OUT" ]; then
probe=$(cat "$X04_03_PROBE_OUT")
printf 'observed: %s\n' "$probe"
if [ "$probe" = 'first=false second=true attempts=2' ]; then
printf 'RESULT: defect reproduced\n'
exit 1
fi
if [ "$probe" = 'first=error second=true attempts=2' ]; then
printf 'RESULT: expected behavior\n'
exit 0
fi
fi
printf 'RESULT: inconclusive\n'
exit 2Expected behavior
The supported preview flow lets the controlling viewer send a device file to the page's open picker (docs/user/remote-access.md#L253-L258). The existing successful-upload test expects the answer to succeed and a fileChooserClosed event; a different ID legitimately returns false (apps/server/src/preview/ServerBrowser.test.ts#L1994-L2029). A failed file delivery should report an upload failure and leave the visible picker state consistent with whether it remains answerable. “No longer open” should describe a missing or invalid picker, not a delivery exception.
Actual behavior
In the streamed preview, the client posts selected files to the preview upload route and clears its picker prompt immediately. When the underlying Playwright FileChooser.setFiles rejects, answerFileChooser returns false; the route maps that value to HTTP 409 with “The page's file picker is no longer open.” The focused test showed that the chooser remains answerable after this response: its second call with the same ID succeeded. The client shows the 409 text as an upload error while its picker prompt remains dismissed.
Impact and scope
Impact: P2? - A failed upload gives the wrong cause, dismisses the client-side picker, and forces the user to activate the page's upload control again before retrying. The service-level contradiction is reproduced; the frequency and exact real-page triggers for setFiles rejection are not established.
This affects streamed preview file uploads on web and mobile when setFiles rejects. A practical workaround is to activate the page's upload control again and retry; success depends on the cause of the original delivery failure. Desktop's native picker path was not evaluated.
Earliest failure stage
Core processing
Analysis or suspected cause
Source trace: chooser validation returns false for a genuinely missing or unauthorized chooser. The same return value is produced by catching a rejection from Playwright, before the code that closes the chooser. Thus the route cannot tell a stale chooser from a file-delivery failure.
Additional context
Interface: UI streamed preview page file picker, backed by the preview HTTP upload route.
The entry path is the apps/web/src/browser/ServerBrowserSurface.tsx#L185-L195, packages/client-runtime/src/preview/serverBrowserStream.ts#L123-L134, and apps/server/src/preview/ServerBrowserStream.ts#L246-L283. The server apps/server/src/preview/ServerBrowser.ts#L1283-L1303; its apps/server/src/preview/ServerBrowser.ts#L1332-L1333. The route treats every false as a stale chooser. The apps/mobile/src/features/browser/PreviewStreamWebView.tsx#L248-L258 also clears its prompt before sending.
Current main retains both the catch-to-false code and the 409 response text. Related picker lifecycle changes and a different open upload issue are listed under References.
Impact
Minor bug or occasional failure
Version or commit
main @ c00a373
Environment
Node.js v26.12.1; vp v1.5.1 using local vite-plus v1.4.0 and Vitest v5.0.5; macOS (Darwin arm64).
Logs or stack traces
The focused run's relevant output, with the checkout's absolute path replaced by <checkout>:
v26.12.1
vp v1.5.1
Local vite-plus:
vite-plus v1.4.0
vitest v5.0.5
RUN v5.0.5 <checkout>
Test Files 1 passed (1)
Tests 1 passed | 73 skipped (74)
observed: first=false second=true attempts=2
RESULT: defect reproducedScreenshots, recordings, or supporting files
No response
Workaround
Activate the page's upload control again and retry. Success depends on the cause of the original delivery failure.
References
- fix(server): a file picker closes when its page is replaced #22062 — closes a picker after page replacement; it does not cover a
setFilesrejection while the chooser remains answerable. - fix(preview): a dropped stream withdraws its stale file picker #29655 — withdraws a stale client picker after stream loss; it does not cover a file delivery error on a live stream.
- [Bug]: Preview upload fails for files over 50 MB when the browser and server are on the same machine #30287 — reports a size limit in agent driven preview uploads rather than a streamed user's picker error.
Activity
juliusmarminge commentedon Nov 12, 2026
I tried to reproduce this on the current build and could not.
On origin/main @ cc97303: (1) Added a case to apps/server/src/preview/ServerBrowser.test.ts that drives the real ServerBrowser service with the existing fake page harness. The controlling viewer is offered a file chooser whose setFiles rejects once, then answerFileChooser is called twice with the same chooser id.
vp test run apps/server/src/preview/ServerBrowser.test.ts -t "file delivery"fails because the first call succeeds withfalse(Exit is not a failure). With that assertion flipped to expect false, the rest passes: the retry with the same id returns true and setFiles was called twice. So the picker was still answerable after the server said it was gone. (2) A real trigger in real headless Chromium (playwright-core 1.64.1): a page with , click, then FileChooser.setFiles with a 60 MB buffer rejects with 'Cannot set buffer larger than 50Mb, please write it to a file and pass its path instead.' The same chooser then accepts a 1-byte file (the page sees it). The upload route accepts files up to 100 MB (UPLOAD_MAX_FILE_BYTES), so a 50-100 MB file chosen in the streamed preview reaches this rejection and gets HTTP 409 'The page's file picker is no longer open.' Not driven end to end through the web or mobile UI, so reproduced is false. Client code shows both clients clear the picker prompt before the upload resolves.Closing as not planned. If it still happens for you, reply with the exact steps and the version you are on and it will be looked at again.