garrytan/gstack · error · Error

load-html: --from-file waitUntil

Error message

load-html: --from-file waitUntil '${json.waitUntil}' invalid

What it means

Validation at write-commands.ts:214-216. The payload's optional waitUntil is present but not one of load, domcontentloaded, networkidle. These are the only Playwright setContent wait strategies the command supports.

Solutions

  1. Set waitUntil to 'load', 'domcontentloaded', or 'networkidle'
  2. Omit waitUntil entirely to accept the default (domcontentloaded)

Example fix

// before — {"html":"...","waitUntil":"idle"}
// after — {"html":"...","waitUntil":"networkidle"}
Defensive patterns

Strategy: validation

Validate before calling

const WAIT_UNTIL = new Set(['load','domcontentloaded','networkidle'])
function validPayloadWaitUntil(w: unknown): boolean {
  return w == null || (typeof w === 'string' && WAIT_UNTIL.has(w))
}

Type guard

function isWaitUntil(v: unknown): v is 'load'|'domcontentloaded'|'networkidle'|undefined {
  return v == null || (typeof v === 'string' && ['load','domcontentloaded','networkidle'].includes(v))
}

Prevention

When it happens

Trigger: Payload JSON like {"html": "...", "waitUntil": "none"} or any value outside the allowed set (e.g. 'idle', 'ready', 'complete').

Common situations: Copying a waitUntil value from another tool's API; guessing a value; typo.

Related errors


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

Appendix: source

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

          // hatch for any caller that can pick the payload path (e.g., an
          // MCP caller issuing load-html with an attacker-influenced path).
          try {
            validateReadPath(path.resolve(payloadPath));
          } catch {
            throw new Error(
              `load-html: --from-file ${payloadPath} must be under ${SAFE_DIRECTORIES.join(' or ')} (security policy). Copy the payload into the project tree or /tmp first.`
            );
          }
          const raw = fs.readFileSync(payloadPath, 'utf8');
          let json: any;
          try { json = JSON.parse(raw); }
          catch (e: any) { throw new Error(`load-html: --from-file JSON parse failed: ${e.message}`); }
          if (typeof json.html !== 'string') {
            throw new Error('load-html: --from-file JSON must have a "html" string field');
          }
          if (json.waitUntil && json.waitUntil !== 'load'
              && json.waitUntil !== 'domcontentloaded' && json.waitUntil !== 'networkidle') {
            throw new Error(`load-html: --from-file waitUntil '${json.waitUntil}' invalid`);
          }
          fromFilePayload = { html: json.html, waitUntil: json.waitUntil };
        } else if (args[i] === '--wait-until') {
          const val = args[++i];
          if (val !== 'load' && val !== 'domcontentloaded' && val !== 'networkidle') {
            throw new Error(`Invalid --wait-until '${val}'. Must be one of: load, domcontentloaded, networkidle.`);
          }
          waitUntil = val;
        } else if (args[i].startsWith('--')) {
          throw new Error(`Unknown flag: ${args[i]}`);
        } else if (!filePath) {
          filePath = args[i];
        }
      }

      // Inline HTML path: validate size + magic byte, then setContent directly.
      if (fromFilePayload) {
        const MAX_BYTES = parseInt(process.env.GSTACK_BROWSE_MAX_HTML_BYTES || '', 10) || (50 * 1024 * 1024);

View on GitHub (pinned to 94993f7401)