{"record":{"id":"665c5c1954803ad0","repo":"jackwener/OpenCLI","slug":"trip-com-attraction-dom-extraction-returned-malfor","errorCode":null,"errorMessage":"Trip.com attraction DOM extraction returned malformed rows","messagePattern":"Trip\\.com attraction DOM extraction returned malformed rows","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/trip/attraction.js","lineNumber":58,"sourceCode":"    func: async (page, kwargs) => {\n        const query = parseKeyword('query', kwargs.query);\n        const limit = parseListLimit(kwargs.limit);\n\n        const searchUrl = buildAttractionSearchUrl(query);\n        await page.goto(searchUrl);\n        const waitResult = await page.evaluate(WAIT_FOR_ATTRACTIONS_JS);\n        if (waitResult === 'captcha') {\n            throw new AuthRequiredError('trip.com', 'Trip.com is asking for a verification; complete it in your browser session and retry');\n        }\n        if (waitResult === 'empty') {\n            throw new EmptyResultError('trip attraction', `No attractions for \"${query}\"`);\n        }\n        if (waitResult !== 'content') {\n            throw new CommandExecutionError(`Trip.com things-to-do page did not render product cards (state=${String(waitResult)})`);\n        }\n        const raw = await page.evaluate(buildAttractionExtractJs());\n        if (!Array.isArray(raw)) {\n            throw new CommandExecutionError('Trip.com attraction DOM extraction returned malformed rows');\n        }\n        if (raw.length === 0) {\n            throw new CommandExecutionError('Trip.com attraction cards rendered but parser did not find required detail-link anchors');\n        }\n        return raw.slice(0, limit).map((r, i) => ({\n            rank: i + 1,\n            name: r.name,\n            rating: r.rating,\n            reviews: r.reviews,\n            booked: r.booked,\n            price: r.price,\n            currency: 'USD',\n            url: r.url,\n        }));\n    },\n});\n","sourceCodeStart":40,"sourceCodeEnd":75,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/trip/attraction.js#L40-L75","documentation":"This CommandExecutionError is thrown when page.evaluate(buildAttractionExtractJs()) returns something other than an array — i.e. the DOM extraction script produced malformed output instead of a list of attraction rows. It indicates the extraction script and the live page DOM are out of sync, not that results are empty (that would be error 3905 or 3902).","triggerScenarios":"Running `trip attraction` when the in-page extraction JS returns null/undefined/an object instead of an array — typically because the things-to-do page markup changed so the script's row-collection logic fails, or evaluate was interrupted.","commonSituations":"Trip.com deployed a redesign of the things-to-do cards so buildAttractionExtractJs no longer collects rows; a partially rendered/interactive-error page where expected DOM containers are absent; a Trip.com error page that still passes the 'content' wait check but has a different structure.","solutions":["Retry once — a transient render race can produce a malformed extraction; persistent failures mean a DOM change.","Open the things-to-do URL in a normal browser and compare card markup against the selectors used by buildAttractionExtractJs in clis/trip/utils.js.","Update the extraction script's selectors to match the new Trip.com card structure (detail-link anchors).","Report/patch the library if Trip.com changed its layout; pin to an older working version meanwhile."],"exampleFix":"// before: assuming array always returned\nconst raw = await page.evaluate(buildAttractionExtractJs());\nconst rows = raw.slice(0, limit);\n// after: caller-side guard\nconst raw = await page.evaluate(buildAttractionExtractJs());\nif (!Array.isArray(raw)) throw new Error('Trip.com extraction returned non-array; selectors likely stale');\nconst rows = raw.slice(0, limit);","handlingStrategy":"type-guard","validationCode":"null","typeGuard":"function isExtractionRows(value) {\n  return Array.isArray(value) && value.every(\n    (r) => r && typeof r === 'object' && typeof r.url === 'string'\n  );\n}","tryCatchPattern":"try {\n  return await runCli(['trip', 'attraction', query]);\n} catch (e) {\n  if (/malformed rows/.test(e.message)) {\n    // selectors stale vs live DOM — surface actionable error, optionally retry once\n    throw new Error('Trip.com attraction extraction is stale; update buildAttractionExtractJs selectors');\n  }\n  throw e;\n}","preventionTips":["Guard any in-page evaluate result with Array.isArray before consuming it.","Watch Trip.com things-to-do markup for redesigns; pin and test selectors regularly.","Distinguish 'malformed rows' (script/DOM mismatch) from 'parser found 0 anchors' (empty parse).","One retry is reasonable, but persistent malformed output needs a selector patch, not retries."],"tags":["dom-parsing","selector-drift","scraping","trip-com"],"backgroundTag":"dom-structure-changed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}