jackwener/OpenCLI · error · CommandExecutionError

Ctrip bus DOM extraction returned malformed rows

Error message

Ctrip bus DOM extraction returned malformed rows

What it means

After the page renders and scrolls, buildBusExtractJs runs in the page to parse schedule rows. If it does not return an array, the extraction is structurally malformed and the CLI throws CommandExecutionError rather than returning garbage. This indicates the page's DOM no longer matches what the extract script expects.

Source

Thrown at clis/ctrip/bus.js:66

        if (fromCity === toCity) {
            throw new ArgumentError(`--from and --to must differ (got ${fromCity})`);
        }
        const date = parseIsoDate('date', kwargs.date);
        const limit = parseListLimit(kwargs.limit);

        const searchUrl = buildBusListUrl(fromCity, toCity, date);
        await page.goto(searchUrl);
        const waitResult = await page.evaluate(WAIT_FOR_BUS_JS);
        if (waitResult === 'captcha') {
            throw new AuthRequiredError('bus.ctrip.com', 'Ctrip is asking for a captcha; complete it in your browser session and retry');
        }
        if (waitResult !== 'content') {
            throw new CommandExecutionError(`Ctrip bus page did not render schedule rows (state=${String(waitResult)})`);
        }
        const renderedCardCount = await page.evaluate(buildScrollUntilJs('.list-item-parent', limit));
        const raw = await page.evaluate(buildBusExtractJs());
        if (!Array.isArray(raw)) {
            throw new CommandExecutionError('Ctrip bus DOM extraction returned malformed rows');
        }
        if (raw.length === 0) {
            if (Number(renderedCardCount) > 0) {
                throw new CommandExecutionError('Ctrip bus rows rendered but parser did not find required schedule anchors');
            }
            throw new EmptyResultError('ctrip bus', `No coaches for ${fromCity} to ${toCity} on ${date}`);
        }
        return raw.slice(0, limit).map((r, i) => ({
            rank: i + 1,
            departureTime: r.departureTime,
            fromStation: r.fromStation,
            toStation: r.toStation,
            duration: r.duration,
            price: r.price,
            status: r.status,
            url: searchUrl,
        }));
    },

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Rerun the command to rule out a transient A/B variant
  2. Inspect the current bus.ctrip.com DOM and update the selectors inside buildBusExtractJs
  3. Pin or update the CLI version so the extract script matches the live Ctrip markup
  4. Check whether the page hit an error variant by logging the page URL/HTML when this fires

Example fix

// before
const raw = await page.evaluate(buildBusExtractJs());
if (!Array.isArray(raw)) { throw new CommandExecutionError('...malformed rows'); }
// after (user-side mitigation: retry once before failing)
let raw = await page.evaluate(buildBusExtractJs());
if (!Array.isArray(raw)) { await page.reload(); raw = await page.evaluate(buildBusExtractJs()); }
Defensive patterns

Strategy: try-catch

Validate before calling

null

Type guard

null

Try / catch

try {
  rows = await ctripBusList({ from, to, date });
} catch (e) {
  if (e instanceof CommandExecutionError && e.message.includes('malformed rows')) {
    console.error('Ctrip DOM no longer matches extract script — update the CLI or pin a working version');
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: `clis ctrip bus` where page.evaluate(buildBusExtractJs()) returns undefined/null/an object instead of an array — typically because the Ctrip list-page markup changed or the script ran against an unexpected page variant.

Common situations: Ctrip front-end redesign moving or renaming row containers; A/B test bucket giving a different DOM; evaluate returning a non-serializable wrapper after an injected-script error.

Understand the failure class

Related errors


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