jackwener/OpenCLI · error · CommandExecutionError

HTTP ${result.httpStatus} from CreateList: ${snippet}

Error message

HTTP ${result.httpStatus} from CreateList: ${snippet}

What it means

This CommandExecutionError is thrown by requireCreateListResult in clis/twitter/list-create.js:49 when the CreateList GraphQL POST returned a non-OK HTTP status that is not 401/403 (those become AuthRequiredError instead). The message embeds the status code and the first 300 chars of the response body so the developer can see Twitter's own error payload. It means Twitter rejected the list-creation request at the HTTP level — the CLI never got far enough to parse a GraphQL result.

Source

Thrown at clis/twitter/list-create.js:49

    if (description.length > DESCRIPTION_MAX) {
        throw new ArgumentError(`Description too long: ${description.length} chars (max ${DESCRIPTION_MAX})`);
    }
    if (modeRaw !== 'public' && modeRaw !== 'private') {
        throw new ArgumentError(`Invalid mode: ${JSON.stringify(kwargs.mode)}. Expected "public" or "private".`);
    }
    return { listName: name, listDescription: description, listMode: modeRaw, privateFlag: modeRaw === 'private' };
}

function requireCreateListResult(result, expectedName, expectedMode) {
    if (!result || typeof result !== 'object') {
        throw new CommandExecutionError(`Unexpected result from twitter list-create: ${JSON.stringify(result)}`);
    }
    if (result.httpStatus === 401 || result.httpStatus === 403) {
        throw new AuthRequiredError('x.com', `Twitter CreateList returned HTTP ${result.httpStatus}`);
    }
    if (!result.ok) {
        const snippet = String(result.bodyText || '').slice(0, 300);
        throw new CommandExecutionError(`HTTP ${result.httpStatus} from CreateList: ${snippet}`);
    }
    if (!result.bodyJson || typeof result.bodyJson !== 'object') {
        throw new CommandExecutionError(`CreateList returned malformed JSON payload. Body: ${String(result.bodyText || '').slice(0, 300)}`);
    }
    const list = result.bodyJson?.data?.list;
    if (!list || typeof list !== 'object') {
        const errors = result.bodyJson?.errors;
        if (Array.isArray(errors) && errors.length > 0) {
            throw new CommandExecutionError(`CreateList failed: ${errors[0].message || JSON.stringify(errors[0])}`);
        }
        throw new CommandExecutionError(`CreateList returned no list payload. Body: ${String(result.bodyText || '').slice(0, 300)}`);
    }
    const id = String(list.id_str || list.id || '');
    if (!/^\d+$/.test(id)) {
        throw new CommandExecutionError('CreateList returned a list payload without a numeric list id.');
    }
    if (typeof list.name !== 'string' || !list.name.trim()) {
        throw new CommandExecutionError('CreateList returned a list payload without a list name.');

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Read the embedded body snippet in the message — it contains Twitter's JSON error explaining the rejection (e.g. rate limit, DecodeException).
  2. If status is 404 or the body mentions queryId/schema mismatch, update CREATE_LIST_QUERY_ID and FEATURES in clis/twitter/list-create.js to match the current x.com web client.
  3. If status is 429, wait and retry later — you have hit Twitter's list-creation rate limit.
  4. If status is 400, verify the request variables (name <= 25 chars, description <= 100 chars, valid mode) — use --mode public|private exactly.
  5. If status is 5xx, retry after a short delay; it is a transient Twitter-side failure.
  6. Confirm you are logged in to x.com in the automation browser session (stale cookies can cause unusual non-401 rejections).

Example fix

// before (drifted queryId causing 404)
const CREATE_LIST_QUERY_ID = 'UQRa0jJ9doxGEIQRea1Y0w';
// after (refreshed from the current x.com web client CreateList request)
const CREATE_LIST_QUERY_ID = 'UQRa0jJ9doxGEIQRea1Y0w_newIdFromLiveCapture';
Defensive patterns

Strategy: try-catch

Validate before calling

// Nothing pre-call can fully prevent server-side rejections, but validate inputs and session first:
if (!name || name.length > 25) throw new Error('List name must be 1-25 chars');
if (description.length > 100) throw new Error('Description must be <= 100 chars');
if (mode !== 'public' && mode !== 'private') throw new Error('mode must be public|private');
// ensure browser session is logged in (ct0 cookie exists) before invoking

Type guard

function isCreateListHttpError(err) {
  return err instanceof Error
    && err.name === 'CommandExecutionError'
    && /^HTTP \d{3} from CreateList:/.test(err.message);
}

Try / catch

try {
  const row = await opencli twitter list-create "My List";
} catch (err) {
  if (/AuthRequiredError/.test(err.name)) { /* re-authenticate x.com */ }
  else if (isCreateListHttpError(err)) {
    const status = Number(err.message.match(/^HTTP (\d{3})/)?.[1]);
    if (status === 429) scheduleRetryWithBackoff();
    else if (status >= 500) retryOnceLater();
    else reportFatal(err.message); // 4xx: body snippet has Twitter's reason
  } else throw err;
}

Prevention

When it happens

Trigger: Any POST to /i/api/graphql/UQRa0jJ9doxGEIQRea1Y0w/CreateList from page.evaluate that resolves with r.ok === false and status not in {401, 403}: e.g. 400 (malformed variables/features), 404 (queryId no longer served), 429 (rate limited), 5xx (Twitter server error).

Common situations: Twitter rotating the CreateList queryId so the hardcoded UQRa0jJ9doxGEIQRea1Y0w becomes stale (404); a schema change making the hardcoded FEATURES set invalid (400 DecodeException); hitting Twitter's write rate limits after creating several lists (429); transient Twitter incidents (5xx); expired/blocked session producing odd non-401 statuses.

Related errors


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