jackwener/OpenCLI · error · CommandExecutionError
Ctrip hotel-search returned malformed SSR hotel list
Error message
Ctrip hotel-search returned malformed SSR hotel list
What it means
A CommandExecutionError raised when the EXTRACT_HOTELS_JS snippet returns null instead of an array — i.e. even after the readiness probe reported 'content', reading window.__NEXT_DATA__.props.pageProps.initListData.hotelList did not yield an array. This is a consistency check: readiness and extraction read the same path, so a mismatch indicates the SSR state changed between probes or the probe/extract paths diverged after a Ctrip layout change.
Source
Thrown at clis/ctrip/hotel-search.js:108
func: async (page, kwargs) => {
const cityId = parseCityId(kwargs.city);
const checkin = parseIsoDate('checkin', kwargs.checkin);
const checkout = parseIsoDate('checkout', kwargs.checkout);
assertCheckinBeforeCheckout(checkin, checkout);
const limit = parseHotelLimit(kwargs.limit);
const url = `https://hotels.ctrip.com/hotels/list?city=${cityId}&checkin=${checkin}&checkout=${checkout}`;
await page.goto(url);
const waitResult = await page.evaluate(WAIT_FOR_SSR_JS);
if (waitResult === 'captcha') {
throw new AuthRequiredError('hotels.ctrip.com', 'Ctrip is asking for a captcha; complete it in your browser session and retry');
}
if (waitResult !== 'content') {
throw new CommandExecutionError(`Ctrip hotel-search page did not expose SSR hotel list (state=${String(waitResult)})`);
}
const raw = await page.evaluate(EXTRACT_HOTELS_JS);
if (!Array.isArray(raw)) {
throw new CommandExecutionError('Ctrip hotel-search returned malformed SSR hotel list');
}
if (raw.length === 0) {
throw new EmptyResultError('ctrip hotel-search', `No hotels for city=${cityId} on ${checkin} → ${checkout}`);
}
const rows = raw
.map((entry, i) => mapHotelRow(entry, i))
.filter((row) => row.hotelId && row.name)
.slice(0, limit);
if (rows.length === 0) {
throw new CommandExecutionError('Ctrip hotel-search SSR rows were missing required hotelId/name anchors');
}
return rows;
},
});
export const __test__ = { parseHotelLimit, assertCheckinBeforeCheckout, WAIT_FOR_SSR_JS, EXTRACT_HOTELS_JS };
View on GitHub (pinned to 49907e53dc)
Solutions
- Retry the search to rule out a transient re-render race.
- Dump window.__NEXT_DATA__ keys in your browser session and update the extraction path in EXTRACT_HOTELS_JS if Ctrip moved initListData.hotelList.
- Make the readiness probe and extraction use a single shared snapshot of the hotelList to eliminate the race.
- Pin/verify the library against the current hotels.ctrip.com layout and report persistent breakage upstream.
Example fix
// before: probe and extract read the page twice
const waitResult = await page.evaluate(WAIT_FOR_SSR_JS);
const raw = await page.evaluate(EXTRACT_HOTELS_JS);
// after: extract data in the same evaluation that waits
const raw = await page.evaluate(`
new Promise((resolve) => {
const detect = () => {
const list = window.__NEXT_DATA__?.props?.pageProps?.initListData?.hotelList;
if (Array.isArray(list)) resolve(list);
};
detect();
const obs = new MutationObserver(() => detect());
obs.observe(document.documentElement, { childList: true, subtree: true });
setTimeout(() => resolve(null), 5000);
})
`); Defensive patterns
Strategy: type-guard
Validate before calling
// Verify the SSR data path is present before/at extraction time
async function extractHotelList(page) {
const list = await page.evaluate(
'window.__NEXT_DATA__?.props?.pageProps?.initListData?.hotelList'
);
return Array.isArray(list) ? list : null;
} Type guard
function isHotelListArray(value) {
return Array.isArray(value) &&
value.every((e) => e != null && typeof e === 'object' &&
e.hotelInfo != null && typeof e.hotelInfo === 'object');
} Try / catch
try {
const rows = await ctripHotelSearch({ city, checkin, checkout });
} catch (e) {
if (e instanceof CommandExecutionError && e.message.includes('malformed SSR hotel list')) {
// layout change or re-render race: retry once, then flag for scraper maintenance
const retried = await ctripHotelSearch({ city, checkin, checkout }).catch(() => null);
if (retried) return retried;
throw new Error('hotels.ctrip.com SSR shape changed — update EXTRACT_HOTELS_JS path');
}
throw e;
} Prevention
- Retry once automatically — SPA re-renders between probe and extract cause transient races.
- Pin/monitor hotels.ctrip.com layout changes that move __NEXT_DATA__ paths.
- Extract the hotel list in the same page evaluation that waits for readiness to avoid races.
- Log window.__NEXT_DATA__ top-level keys on failure to speed up diagnosing schema drift.
When it happens
Trigger: The page's __NEXT_DATA__ was replaced or restructured between the wait probe and the extraction (SPA re-render); a Ctrip deploy renamed/moved initListData.hotelList while the old readiness heuristics still matched via a cached state; page navigation or frame reset wiped window.__NEXT_DATA__ before evaluate ran.
Common situations: Ctrip rolling out a new hotel-list page version; race where client-side routing re-renders the page and resets __NEXT_DATA__; partially loaded hydration payload; regional variants of the list page with a different data shape.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Ctrip hotel-search page did not expose SSR hotel list (state
- ${label} returned an unexpected payload shape; expected an o
- Bilibili ${label} API returned a malformed payload
- Bilibili ${label} API returned malformed data
- Bilibili ${label} API did not return replies
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/0859578700cd7e2c.
Report an issue: GitHub.