jackwener/OpenCLI · error · CommandExecutionError

Trip.com flight DOM extraction returned malformed rows

Error message

Trip.com flight DOM extraction returned malformed rows${reason}

What it means

This CommandExecutionError from clis/trip/flight.js is thrown when buildFlightExtractJs() returns a non-array, meaning the in-page extractor found flight-card-like elements but failed to parse at least one row into a clean object. When the returned object carries an error string matching /^malformed flight card \d+: [a-z /]+$/, that per-card reason is appended to the message. It signals a DOM structure change or unexpected card content, not an empty page.

Solutions

  1. Read the appended reason (if any) to see which card and field was malformed.
  2. Open the page in a browser and inspect the flight card DOM for layout changes.
  3. Update buildFlightExtractJs() field selectors to match the current markup.
  4. Add defensive field extraction (defaulting missing fields) so a single odd card does not abort the whole extraction, then retry.

Example fix

// before
throw new CommandExecutionError(`...malformed rows${reason}`);
// after
if (!Array.isArray(raw)) {
  console.warn('extraction returned', raw);
  raw = []; // or skip malformed cards inside buildFlightExtractJs
}
throw new CommandExecutionError(`...malformed rows${reason}`); // only when unrecoverable
Defensive patterns

Strategy: type-guard

Validate before calling

// Cannot pre-validate server DOM; verify library/site assumptions after upgrade:
const smoke = await runTripFlight({ from: 'SFO', to: 'LAX', date: nextFriday(), limit: 1 });
if (!Array.isArray(smoke)) throw new Error('extractor contract broken');

Type guard

const isExtractFailure = (e) => e instanceof CommandExecutionError && /malformed rows/.test(e.message);
const isArrayPayload = (v) => Array.isArray(v);

Try / catch

try {
  const flights = await runTripFlight(args);
} catch (e) {
  if (isExtractFailure(e)) {
    console.error(`Trip.com DOM may have changed: ${e.message}`);
    return null; // degrade gracefully, alert maintainers
  }
  throw e;
}

Prevention

When it happens

Trigger: page.evaluate(buildFlightExtractJs()) returns null, an object like { error: 'malformed flight card 3: missing airline' }, or any non-array because one or more flight cards lacked expected fields/shape.

Common situations: Trip.com A/B-testing a new flight card layout; ads or sponsored cards injected into the results list with different markup; partially rendered cards missing airline/time nodes when the extractor runs; regional variants of the site with different DOM.

Understand the failure class

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/9ddca788eb88f751. Report an issue: GitHub.

Appendix: source

Thrown at clis/trip/flight.js:68

        const date = parseIsoDate('date', kwargs.date);
        const limit = parseListLimit(kwargs.limit);

        const searchUrl = buildFlightSearchUrl(fromCode, toCode, date);
        await page.goto(searchUrl);
        const waitResult = await page.evaluate(WAIT_FOR_FLIGHTS_JS);
        if (waitResult === 'captcha') {
            throw new AuthRequiredError('trip.com', 'Trip.com is asking for a verification; complete it in your browser session and retry');
        }
        if (waitResult !== 'content') {
            throw new CommandExecutionError(`Trip.com flight page did not render flight cards (state=${String(waitResult)})`);
        }
        const raw = await page.evaluate(buildFlightExtractJs());
        if (!Array.isArray(raw)) {
            const reason = raw && typeof raw === 'object' && typeof raw.error === 'string'
                && /^malformed flight card \d+: [a-z /]+$/.test(raw.error)
                ? `: ${raw.error}`
                : '';
            throw new CommandExecutionError(`Trip.com flight DOM extraction returned malformed rows${reason}`);
        }
        if (raw.length === 0) {
            throw new EmptyResultError('trip flight', `No flights for ${fromCode} to ${toCode} on ${date}`);
        }
        return raw.slice(0, limit).map((r, i) => ({
            rank: i + 1,
            airline: r.airline,
            departureTime: r.departureTime,
            departureAirport: r.departureAirport,
            arrivalTime: r.arrivalTime,
            arrivalAirport: r.arrivalAirport,
            duration: r.duration,
            stops: r.stops,
            price: r.price,
            currency: r.currency,
            url: searchUrl,
        }));
    },

View on GitHub (pinned to 49907e53dc)