jackwener/OpenCLI · error · ArgumentError

Invalid image URL: ${imageUrl}

Error message

Invalid image URL: ${imageUrl}

What it means

downloadRemoteImage first parses the given URL with the URL constructor; if parsing fails it throws ArgumentError `Invalid image URL: <input>`. This catches syntactically malformed URLs before any network request is made.

Source

Thrown at clis/twitter/utils.js:90

    }
    throw new ArgumentError(
        `Unsupported remote image format "${normalizedContentType || 'unknown'}". Supported: jpg, jpeg, png, gif, webp`,
    );
}

/**
 * Download a remote image to a per-call tmp directory. Returns the absolute
 * path on success. Caller owns the tmp dir and must clean it up. Throws
 * {@link ArgumentError} on bad input or download failure.
 *
 * @returns {Promise<{ absPath: string, cleanupDir: string }>}
 */
export async function downloadRemoteImage(imageUrl) {
    let parsed;
    try {
        parsed = new URL(imageUrl);
    } catch {
        throw new ArgumentError(`Invalid image URL: ${imageUrl}`);
    }
    if (!/^https?:$/.test(parsed.protocol)) {
        throw new ArgumentError(`Unsupported image URL protocol: ${parsed.protocol}`);
    }
    const response = await fetch(imageUrl);
    if (!response.ok) {
        throw new ArgumentError(`Image download failed: HTTP ${response.status}`);
    }
    const contentLength = Number(response.headers.get('content-length') || '0');
    if (contentLength > MAX_IMAGE_SIZE_BYTES) {
        throw new ArgumentError(`Image too large: ${(contentLength / 1024 / 1024).toFixed(1)} MB (max ${MAX_IMAGE_SIZE_BYTES / 1024 / 1024} MB)`);
    }
    const ext = resolveImageExtension(imageUrl, response.headers.get('content-type'));
    const cleanupDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-twitter-'));
    const absPath = path.join(cleanupDir, `image${ext}`);
    const buffer = Buffer.from(await response.arrayBuffer());
    if (buffer.byteLength > MAX_IMAGE_SIZE_BYTES) {
        fs.rmSync(cleanupDir, { recursive: true, force: true });

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Ensure the URL includes the scheme: prepend https:// if missing.
  2. Encode the URL: wrap in encodeURI() or quote it in the shell to protect ?& and spaces.
  3. Confirm you are passing a URL, not a local file path — local paths belong to resolveImagePath.
  4. Sanity-check in Node: `new URL(value)` in a REPL should not throw before calling the API.

Example fix

// before
await downloadRemoteImage('cdn.example.com/cat.png'); // no scheme
// after
const url = raw.startsWith('http') ? raw : `https://${raw}`;
await downloadRemoteImage(encodeURI(url));
Defensive patterns

Strategy: validation

Validate before calling

let parsed;
try { parsed = new URL(imageUrl); } catch {
  throw new Error(`Malformed image URL: ${JSON.stringify(imageUrl)}`);
}
if (!parsed.protocol.startsWith('http')) {
  throw new Error(`Scheme must be http(s): ${imageUrl}`);
}

Type guard

function isHttpUrl(value) {
  if (typeof value !== 'string' || value.length === 0) return false;
  try { return new URL(value).protocol.startsWith('http'); } catch { return false; }
}

Try / catch

try {
  await downloadRemoteImage(imageUrl);
} catch (err) {
  if (err instanceof ArgumentError && err.message.startsWith('Invalid image URL')) {
    return downloadRemoteImage(new URL(imageUrl, 'https://example.com').href); // resolve relative
  }
  throw err;
}

Prevention

When it happens

Trigger: Passing a string that `new URL()` cannot parse: missing scheme ("example.com/img.png"), spaces or unencoded special characters, empty string, a file path mistakenly passed to the remote-image path, or undefined/null coerced to a string.

Common situations: Copy-pasting a URL that lost its https:// prefix; shell mangling of '&', '?', or spaces without quotes; forgetting to download a file and passing its local path to the URL-based API; template-string variables that interpolated to empty.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/8734665a318893ce. Report an issue: GitHub.