jackwener/OpenCLI · error · CommandExecutionError

Ctrip train DOM extraction returned malformed rows

Error message

Ctrip train DOM extraction returned malformed rows

What it means

Thrown as CommandExecutionError when buildTrainExtractJs's in-page extraction returns a non-array value instead of a list of train rows. The library treats any non-array extraction result as malformed DOM output and refuses to continue, protecting downstream mapping code from undefined fields.

Source

Thrown at clis/ctrip/train.js:65

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

        const searchUrl = buildTrainListUrl(fromName, toName, date);
        await page.goto(searchUrl);
        const waitResult = await page.evaluate(WAIT_FOR_TRAINS_JS);
        if (waitResult === 'captcha') {
            throw new AuthRequiredError('trains.ctrip.com', 'Ctrip is asking for a captcha; complete it in your browser session and retry');
        }
        if (waitResult !== 'content') {
            throw new CommandExecutionError(`Ctrip train page did not render train cards (state=${String(waitResult)})`);
        }
        const renderedCardCount = await page.evaluate(buildScrollUntilJs('.card-white.list-item', limit));
        const raw = await page.evaluate(buildTrainExtractJs());
        if (!Array.isArray(raw)) {
            throw new CommandExecutionError('Ctrip train DOM extraction returned malformed rows');
        }
        if (raw.length === 0) {
            if (Number(renderedCardCount) > 0) {
                throw new CommandExecutionError('Ctrip train cards rendered but parser did not find required train anchors');
            }
            throw new EmptyResultError('ctrip train', `No trains for ${fromName} to ${toName} on ${date}`);
        }
        return raw.slice(0, limit).map((r, i) => ({
            rank: i + 1,
            trainNo: r.trainNo,
            departureTime: r.departureTime,
            departureStation: r.departureStation,
            arrivalTime: r.arrivalTime,
            arrivalStation: r.arrivalStation,
            duration: r.duration,
            fromPrice: r.fromPrice,
            seats: Array.isArray(r.seats) && r.seats.length ? r.seats.join(' / ') : null,
            url: searchUrl,

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Retry the query; if persistent, inspect the live page DOM against the selectors in buildTrainExtractJs.
  2. Update extraction selectors in buildTrainExtractJs to match current Ctrip markup.
  3. Log the raw evaluate result to see what shape is actually returned.

Example fix

// before
const raw = await page.evaluate(buildTrainExtractJs());
if (!Array.isArray(raw)) throw new CommandExecutionError('malformed rows');
// after
const raw = await page.evaluate(buildTrainExtractJs());
const rows = Array.isArray(raw) ? raw : (raw && Array.isArray(raw.trains) ? raw.trains : null);
if (!rows) throw new CommandExecutionError('malformed rows');
Defensive patterns

Strategy: type-guard

Validate before calling

null

Type guard

const isTrainRows = (v) => Array.isArray(v) && v.every(r => r && typeof r.trainNo === 'string');

Try / catch

try { return await runTrainList(opts); } catch (e) { if (e instanceof CommandExecutionError && /malformed rows/.test(e.message)) { return fallbackScraper(opts); } throw e; }

Prevention

When it happens

Trigger: page.evaluate(buildTrainExtractJs()) returning undefined/null/object because the page context errored, the extraction script's querySelectorAll returned nothing in an unexpected shape, or evaluate serialization failed.

Common situations: Ctrip changing container class names so the extractor's root lookup fails; page.evaluate returning a NodeList-like that isn't a plain array; a prior script error leaving the extractor's return value undefined.

Understand the failure class

Related errors


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