garrytan/gstack · error · Error

Invalid file URL: ${url} (${e.message})

Error message

Invalid file URL: ${url} (${e.message})

What it means

Wraps a failure from Node's fileURLToPath(parsed) at url-validation.ts:255-258. The URL already parsed successfully and passed the host check, but Node's own file-URL-to-path conversion rejected it. The appended e.message is the underlying Node error, which is the key to the real cause.

Source

Thrown at browse/src/url-validation.ts:258

  }

  // file:// path: validate against safe-dirs and allow; otherwise defer to http(s) logic.
  if (parsed.protocol === 'file:') {
    // Reject non-empty non-localhost hosts (UNC / network paths).
    if (parsed.host !== '' && parsed.host.toLowerCase() !== 'localhost') {
      throw new Error(
        `Unsupported file URL host: ${parsed.host}. Use file:///<absolute-path> for local files.`
      );
    }

    // Convert URL → filesystem path with proper decoding (handles %20, %2F, etc.)
    // fileURLToPath strips query + hash; we reattach them after validation so SPA
    // fixture URLs like file:///tmp/app.html?route=home#login survive intact.
    let fsPath: string;
    try {
      fsPath = fileURLToPath(parsed);
    } catch (e: any) {
      throw new Error(`Invalid file URL: ${url} (${e.message})`);
    }

    // Reject path traversal after decoding — e.g. file:///tmp/safe%2F..%2Fetc/passwd
    // Note: fileURLToPath doesn't collapse .., so a literal '..' in the decoded path
    // is suspicious. path.resolve will normalize it; check the result against safe dirs.
    validateReadPath(fsPath);

    // Return the canonical file:// URL derived from the filesystem path + original
    // query + hash. This guarantees page.goto() gets a well-formed URL regardless
    // of input shape while preserving SPA route/query params.
    return pathToFileURL(fsPath).href + parsed.search + parsed.hash;
  }

  if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
    throw new Error(
      `Blocked: scheme "${parsed.protocol}" is not allowed. Only http:, https:, and file: URLs are permitted.`
    );
  }

View on GitHub (pinned to 94993f7401)

Solutions

  1. Read the appended Node error message first — it names the exact malformation
  2. Construct the URL with pathToFileURL(absolutePath).href rather than string-building it
  3. Pass a plain absolute filesystem path and let the library form the file URL
  4. On Windows, ensure drive letters are present (file:///C:/...) and avoid forward-slash roots

Example fix

// before (hand-built, fails fileURLToPath on Windows)
await goto('file://' + rawPath)
// after
import { pathToFileURL } from 'node:url'
await goto(pathToFileURL(rawPath).href)
Defensive patterns

Strategy: validation

Validate before calling

import { fileURLToPath, pathToFileURL } from 'node:url'
// Pre-validate that Node can convert the file URL to a path.
function isValidFileUrl(u: string): boolean {
  try { fileURLToPath(new URL(u)); return true } catch { return false }
}

Type guard

function isConvertibleFileUrl(u: string): u is string {
  try { fileURLToPath(new URL(u)); return true } catch { return false }
}

Try / catch

try {
  await goto(normalizedUrl)
} catch (e) {
  if (e.message.startsWith('Invalid file URL:')) {
    // rebuild canonically and retry once
    await goto(pathToFileURL(absPath).href)
  } else throw e
}

Prevention

When it happens

Trigger: A file: URL that new URL() accepts but fileURLToPath rejects: malformed percent-encoding, an incompatible Windows drive-letter form, or a path that cannot be represented on the current OS. Most common on Windows with non-drive roots like file:///usr/local or bad %2F sequences.

Common situations: Cross-platform path strings rendered into file URLs naively; string concatenation that produces double-encoded sequences; Windows builds receiving Unix-style roots.

Related errors


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