garrytan/gstack · error · Error

File not found: ${fp}

Error message

File not found: ${fp}

What it means

Thrown by `browse upload` inside the per-file validation loop when `fs.existsSync(fp)` returns false for one of the supplied file paths. The command refuses to forward a non-existent path to Playwright's `setInputFiles`, which would itself throw a less actionable error. Note the existence check runs BEFORE the safe-directory and traversal checks.

Source

Thrown at browse/src/write-commands.ts:600

    case 'useragent': {
      const ua = args.join(' ');
      if (!ua) throw new Error('Usage: browse useragent <string>');
      bm.setUserAgent(ua);
      const error = await bm.recreateContext();
      if (error) {
        return `User agent set to "${ua}" but: ${error}`;
      }
      return `User agent set: ${ua}`;
    }

    case 'upload': {
      const [selector, ...filePaths] = args;
      if (!selector || filePaths.length === 0) throw new Error('Usage: browse upload <selector> <file1> [file2...]');

      // Validate paths are within safe directories (same check as cookie-import)
      for (const fp of filePaths) {
        if (!fs.existsSync(fp)) throw new Error(`File not found: ${fp}`);
        if (path.isAbsolute(fp)) {
          let resolvedFp: string;
          try { resolvedFp = fs.realpathSync(path.resolve(fp)); } catch (err: any) { if (err?.code !== 'ENOENT') throw err; resolvedFp = path.resolve(fp); }
          if (!SAFE_DIRECTORIES.some(dir => isPathWithin(resolvedFp, dir))) {
            throw new Error(`Path must be within: ${SAFE_DIRECTORIES.join(', ')}`);
          }
        }
        if (path.normalize(fp).includes('..')) {
          throw new Error('Path traversal sequences (..) are not allowed');
        }
      }

      const resolved = await session.resolveRef(selector);
      if ('locator' in resolved) {
        await resolved.locator.setInputFiles(filePaths);
      } else {
        await target.locator(resolved.selector).setInputFiles(filePaths);
      }

View on GitHub (pinned to 94993f7401)

Solutions

  1. Verify the path exists from the same process/cwd the browse server uses: `fs.existsSync(path.resolve(fp))`.
  2. Expand `~` to `os.homedir()` before passing — Node does not do this for you.
  3. Use an absolute path under TEMP_DIR or the project cwd (those are the safe directories).
  4. If running in a container, ensure the file is volume-mounted into the container at the path you reference.

Example fix

// before
await runBrowseCommand(['upload', 'input[type=file]', '~/Downloads/x.png']);

// after
import os from 'os';
import path from 'path';
const fp = path.join(os.homedir(), 'Downloads', 'x.png');
await runBrowseCommand(['upload', 'input[type=file]', fp]);
Defensive patterns

Strategy: validation

Validate before calling

import fs from 'fs';
import path from 'path';
function ensureFilesExist(filePaths: string[], cwd = process.cwd()): void {
  for (const fp of filePaths) {
    const resolved = path.resolve(cwd, fp);
    if (!fs.existsSync(resolved)) {
      throw new Error(`File not found: ${fp} (resolved: ${resolved})`);
    }
  }
}

Type guard

function fileExists(p: string): boolean {
  try { return fs.statSync(p).isFile(); } catch { return false; }
}

Prevention

When it happens

Trigger: Passing a relative path that does not resolve against the process cwd; a typo'd filename; a path on a different machine (remote agent context); a file that was deleted between snapshot time and upload time; a path with an unexpanded `~` (tilde is NOT expanded by Node's fs APIs).

Common situations: Agent captured an absolute path on the host but the browse server runs in a container with a different filesystem layout; user passes `~/Downloads/x.png` and Node does not expand `~`; file lives in a directory the user just `rm -rf`'d during cleanup; macOS case-insensitive FS masked a casing mismatch at authoring time but the browse server is on Linux (case-sensitive).

Related errors


AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12). Data as JSON: /api/errors/f9ff1ff1b29d0cbb. Report an issue: GitHub.