{"record":{"id":"8b386ac94826ad17","repo":"jackwener/OpenCLI","slug":"ctrip-train-dom-extraction-returned-malformed-rows","errorCode":null,"errorMessage":"Ctrip train DOM extraction returned malformed rows","messagePattern":"Ctrip train DOM extraction returned malformed rows","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/ctrip/train.js","lineNumber":65,"sourceCode":"        if (fromName === toName) {\n            throw new ArgumentError(`--from and --to must differ (got ${fromName})`);\n        }\n        const date = parseIsoDate('date', kwargs.date);\n        const limit = parseListLimit(kwargs.limit);\n\n        const searchUrl = buildTrainListUrl(fromName, toName, date);\n        await page.goto(searchUrl);\n        const waitResult = await page.evaluate(WAIT_FOR_TRAINS_JS);\n        if (waitResult === 'captcha') {\n            throw new AuthRequiredError('trains.ctrip.com', 'Ctrip is asking for a captcha; complete it in your browser session and retry');\n        }\n        if (waitResult !== 'content') {\n            throw new CommandExecutionError(`Ctrip train page did not render train cards (state=${String(waitResult)})`);\n        }\n        const renderedCardCount = await page.evaluate(buildScrollUntilJs('.card-white.list-item', limit));\n        const raw = await page.evaluate(buildTrainExtractJs());\n        if (!Array.isArray(raw)) {\n            throw new CommandExecutionError('Ctrip train DOM extraction returned malformed rows');\n        }\n        if (raw.length === 0) {\n            if (Number(renderedCardCount) > 0) {\n                throw new CommandExecutionError('Ctrip train cards rendered but parser did not find required train anchors');\n            }\n            throw new EmptyResultError('ctrip train', `No trains for ${fromName} to ${toName} on ${date}`);\n        }\n        return raw.slice(0, limit).map((r, i) => ({\n            rank: i + 1,\n            trainNo: r.trainNo,\n            departureTime: r.departureTime,\n            departureStation: r.departureStation,\n            arrivalTime: r.arrivalTime,\n            arrivalStation: r.arrivalStation,\n            duration: r.duration,\n            fromPrice: r.fromPrice,\n            seats: Array.isArray(r.seats) && r.seats.length ? r.seats.join(' / ') : null,\n            url: searchUrl,","sourceCodeStart":47,"sourceCodeEnd":83,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/ctrip/train.js#L47-L83","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Retry the query; if persistent, inspect the live page DOM against the selectors in buildTrainExtractJs.","Update extraction selectors in buildTrainExtractJs to match current Ctrip markup.","Log the raw evaluate result to see what shape is actually returned."],"exampleFix":"// before\nconst raw = await page.evaluate(buildTrainExtractJs());\nif (!Array.isArray(raw)) throw new CommandExecutionError('malformed rows');\n// after\nconst raw = await page.evaluate(buildTrainExtractJs());\nconst rows = Array.isArray(raw) ? raw : (raw && Array.isArray(raw.trains) ? raw.trains : null);\nif (!rows) throw new CommandExecutionError('malformed rows');","handlingStrategy":"type-guard","validationCode":"null","typeGuard":"const isTrainRows = (v) => Array.isArray(v) && v.every(r => r && typeof r.trainNo === 'string');","tryCatchPattern":"try { return await runTrainList(opts); } catch (e) { if (e instanceof CommandExecutionError && /malformed rows/.test(e.message)) { return fallbackScraper(opts); } throw e; }","preventionTips":["Keep buildTrainExtractJs selectors in sync with Ctrip DOM","Log raw evaluate output when debugging","Wrap evaluate calls to normalize NodeList-like returns to arrays"],"tags":["scraping","dom-extraction","page-structure"],"backgroundTag":"unexpected-dom-shape","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}