{"record":{"id":"4adab75997c5e760","repo":"jackwener/OpenCLI","slug":"trip-com-flight-dom-extraction-returned-malformed","errorCode":null,"errorMessage":"Trip.com flight DOM extraction returned malformed rows${reason}","messagePattern":"Trip\\.com flight DOM extraction returned malformed rows(.+?)","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/trip/flight-round.js","lineNumber":73,"sourceCode":"        }\n        const limit = parseListLimit(kwargs.limit);\n\n        const searchUrl = buildFlightRoundSearchUrl(fromCode, toCode, depart, ret);\n        await page.goto(searchUrl);\n        const waitResult = await page.evaluate(WAIT_FOR_FLIGHTS_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 !== 'content') {\n            throw new CommandExecutionError(`Trip.com flight page did not render flight cards (state=${String(waitResult)})`);\n        }\n        const raw = await page.evaluate(buildFlightExtractJs());\n        if (!Array.isArray(raw)) {\n            const reason = raw && typeof raw === 'object' && typeof raw.error === 'string'\n                && /^malformed flight card \\d+: [a-z /]+$/.test(raw.error)\n                ? `: ${raw.error}`\n                : '';\n            throw new CommandExecutionError(`Trip.com flight DOM extraction returned malformed rows${reason}`);\n        }\n        if (raw.length === 0) {\n            throw new EmptyResultError('trip flight-round', `No round-trip flights for ${fromCode} to ${toCode} on ${depart} .. ${ret}`);\n        }\n        return raw.slice(0, limit).map((r, i) => ({\n            rank: i + 1,\n            airline: r.airline,\n            departureTime: r.departureTime,\n            departureAirport: r.departureAirport,\n            arrivalTime: r.arrivalTime,\n            arrivalAirport: r.arrivalAirport,\n            duration: r.duration,\n            stops: r.stops,\n            price: r.price,\n            currency: r.currency,\n            url: searchUrl,\n        }));\n    },","sourceCodeStart":55,"sourceCodeEnd":91,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/trip/flight-round.js#L55-L91","documentation":"CommandExecutionError thrown when buildFlightExtractJs() returns a non-array value: the flight-card extractor failed rather than producing a row list. When the non-array value carries an in-page error matching /^malformed flight card \\d+: [a-z /]+$/, that per-card reason is appended to the message, pinpointing which card and field failed to parse.","triggerScenarios":"page.evaluate(buildFlightExtractJs()) resolves to something other than an array — e.g. {error: 'malformed flight card 3: missing price'} — after flights rendered but a card lacked an expected field; empty results take a different path (EmptyResultError) and never reach this throw.","commonSituations":"Partially rendered results where some cards lack price/duration nodes; Trip.com mixing sponsored cards with different markup into the results list; extractor field regexes too strict for a new card variant; mid-navigation aborting evaluation.","solutions":["Read the appended reason (e.g. 'malformed flight card 3: missing price') to identify the failing card/field and rerun — partial rendering often resolves on retry","Loosen or add fallback selectors in buildFlightExtractJs for the field reported missing","Filter sponsored/promo cards before extraction so variant markup is skipped","Update the CLI package if the extractor has been fixed upstream"],"exampleFix":"// before\nconst reason = raw && typeof raw === 'object' && typeof raw.error === 'string'\n    && /^malformed flight card \\d+: [a-z /]+$/.test(raw.error) ? `: ${raw.error}` : '';\nthrow new CommandExecutionError(`Trip.com flight DOM extraction returned malformed rows${reason}`);\n// after\n// In buildFlightExtractJs, skip cards missing optional fields instead of failing:\n// const price = card.querySelector('.price')?.textContent?.trim();\n// if (!price) return null; // filter nulls in the caller\nconst rows = raw.filter(Boolean);","handlingStrategy":"try-catch","validationCode":"// Confirm results actually rendered and look complete before extraction\nconst cardCount = await page.evaluate(() =>\n    document.querySelectorAll('[class*=flight-card], [data-flight-id]').length);\nif (cardCount > 0) console.log(`${cardCount} flight cards rendered; extractor should parse all or fail with a per-card reason`);","typeGuard":"function isFlightRows(v) {\n    return Array.isArray(v) && v.every(r => r && typeof r === 'object' && 'rank' in r);\n}\nfunction hasCardError(v): v is { error: string } {\n    return !!v && typeof v === 'object' && typeof (v as any).error === 'string';\n}","tryCatchPattern":"try {\n    const flights = await runFlightRound(args);\n} catch (e) {\n    if (e instanceof CommandExecutionError && /malformed rows/.test(e.message)) {\n        // log e.message (includes per-card reason) and retry once; persistent failures mean markup drift\n    } else { throw e; }\n}","preventionTips":["Make the extractor skip (return null for) cards missing optional fields instead of aborting","Filter sponsored/variant cards before field extraction","Log the appended per-card reason and monitor it to catch markup changes early"],"tags":["scraping","dom-parsing","markup-drift","trip-com"],"backgroundTag":"unexpected-extract-shape","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}