{"record":{"id":"a6ac24da41571599","repo":"jackwener/OpenCLI","slug":"ctrip-ferry-dom-extraction-returned-malformed-rows","errorCode":null,"errorMessage":"Ctrip ferry DOM extraction returned malformed rows","messagePattern":"Ctrip ferry DOM extraction returned malformed rows","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/ctrip/ferry.js","lineNumber":66,"sourceCode":"        if (fromCity === toCity) {\n            throw new ArgumentError(`--from and --to must differ (got ${fromCity})`);\n        }\n        const date = parseIsoDate('date', kwargs.date);\n        const limit = parseListLimit(kwargs.limit);\n\n        const searchUrl = buildFerryListUrl(fromCity, toCity, date);\n        await page.goto(searchUrl);\n        const waitResult = await page.evaluate(WAIT_FOR_FERRY_JS);\n        if (waitResult === 'captcha') {\n            throw new AuthRequiredError('ship.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 ferry page did not render sailing rows (state=${String(waitResult)})`);\n        }\n        const renderedCardCount = await page.evaluate(buildScrollUntilJs('.list-item-parent', limit));\n        const raw = await page.evaluate(buildFerryExtractJs());\n        if (!Array.isArray(raw)) {\n            throw new CommandExecutionError('Ctrip ferry DOM extraction returned malformed rows');\n        }\n        if (raw.length === 0) {\n            if (Number(renderedCardCount) > 0) {\n                throw new CommandExecutionError('Ctrip ferry rows rendered but parser did not find required sailing anchors');\n            }\n            throw new EmptyResultError('ctrip ferry', `No sailings for ${fromCity} to ${toCity} on ${date}`);\n        }\n        return raw.slice(0, limit).map((r, i) => ({\n            rank: i + 1,\n            shipName: r.shipName,\n            departureTime: r.departureTime,\n            fromPort: r.fromPort,\n            arrivalTime: r.arrivalTime,\n            toPort: r.toPort,\n            duration: r.duration,\n            price: r.price,\n            status: r.status,\n            url: searchUrl,","sourceCodeStart":48,"sourceCodeEnd":84,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/ctrip/ferry.js#L48-L84","documentation":"CommandExecutionError thrown when buildFerryExtractJs's page.evaluate resolves to a non-array value — the in-page extraction script broke its contract of returning an array of sailing rows. Unlike the parse-empty case, nothing about the data was judged; the extraction itself returned structurally invalid output, usually because the serialized function threw in-page or was mangled before evaluation.","triggerScenarios":"Calling `ctrip ferry <from> <to> --date <d>` where page.evaluate(buildFerryExtractJs()) returns undefined/null/non-array — extraction script threw inside the page (bridge swallows the throw and returns undefined), the function was transpiled into something unserializable, or the evaluation raced a page redirect away from the results URL.","commonSituations":"Bundling/minifying clis/ctrip/utils.js so the function passed to page.evaluate loses its shape; CSP or injected page scripts interfering with evaluate; CLI/utils version mismatch after partial upgrade; SPA redirect firing between the scroll step and extraction.","solutions":["Rerun to rule out a one-off race; if reproducible, inspect buildFerryExtractJs in clis/ctrip/utils.js and confirm every code path returns an Array","Run the CLI unbundled (raw ESM via node) — function serialization to page.evaluate is fragile under bundlers/transpilers","Log the current page URL at extraction time to detect redirects happening mid-command","Capture console errors from the page during evaluate to find the in-page exception"],"exampleFix":"// before (utils.js): throw inside evaluate -> bridge returns undefined\nreturn rows.map(r => {\n  if (!r.ship) throw new Error('missing');\n  return {...};\n});\n// after: never throw in-page; filter instead\nreturn rows.filter(r => r.ship).map(r => ({...}));","handlingStrategy":"type-guard","validationCode":"// Verify the extractor is a plain serializable function before evaluation\nimport { buildFerryExtractJs } from './utils.js';\nif (typeof buildFerryExtractJs !== 'function') throw new Error('buildFerryExtractJs missing');","typeGuard":"function isNonArrayExtraction(v) {\n  return v === null || v === undefined || !Array.isArray(v);\n}\nconst raw = await page.evaluate(buildFerryExtractJs());\nif (isNonArrayExtraction(raw)) throw new Error('ferry extractor returned non-array');","tryCatchPattern":"try {\n  return await run(['ctrip', 'ferry', '--from', f, '--to', t, '--date', d]);\n} catch (e) {\n  if (e instanceof Error && /malformed rows/.test(e.message)) {\n    console.error('Extraction contract broken: run unbundled, check buildFerryExtractJs return types');\n    throw e;\n  }\n  throw e;\n}","preventionTips":["Keep helper modules unbundled/unminified — page.evaluate serialization is fragile","Make buildFerryExtractJs return an explicit Array on every path; never throw inside evaluate","Log location.href right before extraction to catch mid-command SPA redirects","Capture page console errors during evaluate to surface in-page exceptions"],"tags":["dom-extraction","parser","contract-violation","ferry","ctrip"],"backgroundTag":"dom-extraction-mismatch","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}