{"record":{"id":"4a8ea377ee4893b3","repo":"jackwener/OpenCLI","slug":"ctrip-cruise-dom-extraction-returned-malformed-row","errorCode":null,"errorMessage":"Ctrip cruise DOM extraction returned malformed rows","messagePattern":"Ctrip cruise DOM extraction returned malformed rows","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/ctrip/cruise.js","lineNumber":80,"sourceCode":"        let searchUrl = indexUrl;\n        if (portCode !== PORT_INDEX_CODE) {\n            searchUrl = buildCruiseSearchUrl(portCode);\n            await page.goto(searchUrl);\n            const portWait = await page.evaluate(WAIT_FOR_CRUISE_JS);\n            if (portWait === 'captcha') {\n                throw new AuthRequiredError('cruise.ctrip.com', 'Ctrip is asking for a captcha; complete it in your browser session and retry');\n            }\n            if (portWait === 'empty') {\n                throw new EmptyResultError('ctrip cruise', `No cruises currently departing \"${port}\"`);\n            }\n            if (portWait !== 'content') {\n                throw new CommandExecutionError(`Ctrip cruise port page did not render (state=${String(portWait)})`);\n            }\n        }\n\n        const raw = await page.evaluate(buildCruiseExtractJs());\n        if (!Array.isArray(raw)) {\n            throw new CommandExecutionError('Ctrip cruise DOM extraction returned malformed rows');\n        }\n        if (raw.length === 0) {\n            throw new CommandExecutionError('Ctrip cruise cards rendered but parser did not find required itinerary anchors');\n        }\n        return raw.slice(0, limit).map((r, i) => ({\n            rank: i + 1,\n            title: r.title,\n            star: r.star,\n            boarding: r.boarding,\n            sailingDate: r.sailingDate,\n            tags: r.tags,\n            price: r.price,\n            url: searchUrl,\n        }));\n    },\n});\n","sourceCodeStart":62,"sourceCodeEnd":97,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/ctrip/cruise.js#L62-L97","documentation":"CommandExecutionError thrown when buildCruiseExtractJs's page.evaluate returns a non-array value, meaning the in-page extraction script did not return the expected array of cruise rows. This is a parser-contract violation: the extraction JS itself failed or was subverted (e.g. page context returned something unexpected, script serialization failed, or Ctrip injected conflicting globals). It signals the CLI's DOM contract with the page broke at the structural level.","triggerScenarios":"Calling `ctrip cruise <port>` where page.evaluate(buildCruiseExtractJs()) resolves to undefined/null/non-array — typically because the extraction script threw in-page and the bridge returned undefined, or the evaluated function was mangled (minifier/transpile issue) so it no longer returns an array.","commonSituations":"Bundling/transpiling clis/ctrip/utils.js in a way that breaks the serialized function passed to page.evaluate; page CSP or injected scripts interfering with evaluation; version mismatch where the CLI and its helpers were partially upgraded; a page redirect landing on a non-results URL right before extraction.","solutions":["Rerun the command — if it reproduces consistently, the extraction script itself is broken, not the page data","Verify buildCruiseExtractJs in clis/ctrip/utils.js returns an explicit array (e.g. `return rows;` where rows is an Array) and is not transpiled into an unserializable closure","Check you are running the CLI unbundled as intended (raw ESM via node), since function serialization to page.evaluate is fragile under bundling","Log the actual page URL at extraction time to rule out a redirect to a non-results page"],"exampleFix":"// before (utils.js): implicit/fragile return\nconst rows = document.querySelectorAll('.route_info');\nreturn { rows }; // object, not array -> triggers 903\n// after\nconst rows = [...document.querySelectorAll('.route_info')];\nreturn rows.map(r => ({...})); // always an array","handlingStrategy":"type-guard","validationCode":"// Validate the extraction helper exists and is a plain function before the CLI evaluates it\nimport { buildCruiseExtractJs } from './utils.js';\nif (typeof buildCruiseExtractJs !== 'function') throw new Error('buildCruiseExtractJs missing');","typeGuard":"function isNonArrayExtraction(v) {\n  return v === null || v === undefined || !Array.isArray(v);\n}\n// in wrapper code:\nconst raw = await page.evaluate(buildCruiseExtractJs());\nif (isNonArrayExtraction(raw)) throw new Error('extractor returned non-array');","tryCatchPattern":"try {\n  return await run(['ctrip', 'cruise', port]);\n} catch (e) {\n  if (e instanceof Error && /malformed rows/.test(e.message)) {\n    console.error('Extraction contract broken: run CLI unbundled and check buildCruiseExtractJs');\n    throw e;\n  }\n  throw e;\n}","preventionTips":["Never bundle/minify the helper modules passed into page.evaluate — serialization breaks closures","Ensure buildCruiseExtractJs returns an explicit Array on every code path","Pin the opencli version and test extraction after any upgrade","Check for mid-command page redirects by logging location.href at extraction time"],"tags":["dom-extraction","parser","contract-violation","ctrip","browser-automation"],"backgroundTag":"dom-extraction-mismatch","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}