jackwener/OpenCLI · error · CommandExecutionError

Twitter reply completion returned a malformed status url.

Error message

Twitter reply completion returned a malformed status url.

What it means

validateReplyStatusUrl parses the status URL reported after a reply submit with `new URL(result.url)` and rejects it if parsing throws. X sometimes reports a success permalink that is not an absolute, parseable URL (relative path, bare ID, or truncated string). The library throws this CommandExecutionError so callers know the reply may have posted but confirmation is unreliable.

Source

Thrown at clis/twitter/reply.js:51

    if (!result || typeof result !== 'object' || Array.isArray(result) || typeof result.ok !== 'boolean') {
        throw new CommandExecutionError(`${context} returned a malformed result.`);
    }
    if (Object.prototype.hasOwnProperty.call(result, 'message') && result.message != null && typeof result.message !== 'string') {
        throw new CommandExecutionError(`${context} returned a malformed message.`);
    }
    if (Object.prototype.hasOwnProperty.call(result, 'url') && result.url != null && typeof result.url !== 'string') {
        throw new CommandExecutionError(`${context} returned a malformed status url.`);
    }
    return result;
}

function validateReplyStatusUrl(result) {
    if (!result.url) return;
    let url;
    try {
        url = new URL(result.url);
    } catch {
        throw new CommandExecutionError('Twitter reply completion returned a malformed status url.');
    }
    const hostname = url.hostname.toLowerCase().replace(/^www\./, '');
    const match = url.pathname.match(/^\/([^/]+)\/status\/(\d+)\/?$/);
    if (!['x.com', 'twitter.com', 'mobile.twitter.com'].includes(hostname) || !match) {
        throw new CommandExecutionError('Twitter reply completion returned a malformed status url.');
    }
}

async function openReplyComposer(page, rawUrl) {
    await page.goto(buildReplyComposerUrl(rawUrl), { waitUntil: 'load', settleMs: 2500 });
    try {
        await page.wait({ selector: COMPOSER_SELECTOR, timeout: 15 });
        return { ok: true };
    } catch {
        // X sometimes leaves /compose/post?in_reply_to=<id> on the Home
        // timeline behind a loading dialog. Fall back to the canonical tweet
        // page and click the visible Reply action there.
        await page.goto(rawUrl, { waitUntil: 'load', settleMs: 2500 });

View on GitHub (pinned to 49907e53dc)

Solutions

  1. In the page script, always return an absolute URL: `url: location.origin + element.getAttribute('href')` or `element.href`.
  2. Prefix relative values before parsing: `new URL(result.url, 'https://x.com')` in validateReplyStatusUrl.
  3. Re-run the reply command — a transient race can yield a partial permalink; check the profile to see whether the reply actually posted before retrying.

Example fix

// before
try { url = new URL(result.url); } catch { throw ... }
// after
try { url = new URL(result.url, 'https://x.com'); } catch { throw ... }
Defensive patterns

Strategy: validation

Validate before calling

function isParseableUrl(u) {
  try { new URL(u); return true; } catch { return false; }
}
// precondition on reported permalink:
if (result.url && !isParseableUrl(result.url)) {
  // normalize before calling the command or treat confirmation as failed
  result.url = new URL(result.url, 'https://x.com').href;
}

Type guard

function isAbsoluteHttpUrl(u) {
  if (typeof u !== 'string') return false;
  try { const url = new URL(u); return url.protocol === 'https:'; } catch { return false; }
}

Try / catch

try {
  await cli('twitter', 'reply', { url, text });
} catch (e) {
  if (String(e.message).includes('malformed status url')) {
    // reply may have posted; verify on profile before retrying
  } else throw e;
}

Prevention

When it happens

Trigger: The page's post-submit detection returns a url like `/username/status/123` (relative), `1234567890` (bare ID), an empty-ish template string, or any value that fails `new URL()` construction.

Common situations: X DOM changes so the script scrapes an href fragment instead of the full permalink; an in-page snippet returning `element.pathname` instead of `element.href`; race conditions reading a not-yet-populated permalink element.

Understand the failure class

Related errors


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