{"record":{"id":"cc7c174aab50e801","repo":"jackwener/OpenCLI","slug":"ctrip-hotel-search-ssr-rows-were-missing-required","errorCode":null,"errorMessage":"Ctrip hotel-search SSR rows were missing required hotelId/name anchors","messagePattern":"Ctrip hotel-search SSR rows were missing required hotelId/name anchors","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/ctrip/hotel-search.js","lineNumber":118,"sourceCode":"        if (waitResult === 'captcha') {\n            throw new AuthRequiredError('hotels.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 hotel-search page did not expose SSR hotel list (state=${String(waitResult)})`);\n        }\n        const raw = await page.evaluate(EXTRACT_HOTELS_JS);\n        if (!Array.isArray(raw)) {\n            throw new CommandExecutionError('Ctrip hotel-search returned malformed SSR hotel list');\n        }\n        if (raw.length === 0) {\n            throw new EmptyResultError('ctrip hotel-search', `No hotels for city=${cityId} on ${checkin} → ${checkout}`);\n        }\n        const rows = raw\n            .map((entry, i) => mapHotelRow(entry, i))\n            .filter((row) => row.hotelId && row.name)\n            .slice(0, limit);\n        if (rows.length === 0) {\n            throw new CommandExecutionError('Ctrip hotel-search SSR rows were missing required hotelId/name anchors');\n        }\n        return rows;\n    },\n});\n\nexport const __test__ = { parseHotelLimit, assertCheckinBeforeCheckout, WAIT_FOR_SSR_JS, EXTRACT_HOTELS_JS };\n","sourceCodeStart":100,"sourceCodeEnd":125,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/ctrip/hotel-search.js#L100-L125","documentation":"CommandExecutionError thrown when the extracted SSR rows all fail the filter(row => row.hotelId && row.name), i.e. after mapping, no row carries both a hotelId and a name. The list was an array (so it parsed) but its entries lack the two required anchor fields, so the library refuses to return empty-looking data.","triggerScenarios":"EXTRACT_HOTELS_JS returns an array whose objects use different key names (e.g. hotelName/id) or whose fields are nested, so mapHotelRow cannot anchor hotelId/name on any entry; also when Ctrip serves promo/placeholder cards instead of real hotels.","commonSituations":"Ctrip API shape drift (renamed fields); region-specific pages with different card layouts; holiday/promotional pages with skeleton cards; a stale mapHotelRow mapping table.","solutions":["Print one raw entry and update mapHotelRow to map the current field names to hotelId/name","Update the CLI to a version matching current Ctrip markup","Check whether the search params (cityId/checkin/checkout) returned only placeholder cards and use a normal city/date combo"],"exampleFix":"// before\nconst rows = raw.map((entry, i) => mapHotelRow(entry, i)).filter((row) => row.hotelId && row.name);\n// after\nconst rows = raw.map((entry, i) => mapHotelRow(entry, i))\n  .map((row) => ({ ...row, hotelId: row.hotelId ?? row.hotelID ?? row.id, name: row.name ?? row.hotelName }))\n  .filter((row) => row.hotelId && row.name);","handlingStrategy":"type-guard","validationCode":"const rows = raw.map(mapHotelRow).filter((r) => r.hotelId && r.name);\nif (rows.length === 0) console.warn('no rows carried hotelId/name anchors — inspect raw entries');","typeGuard":"const hasAnchors = (row) => typeof row === 'object' && row !== null && typeof row.hotelId === 'string' && row.hotelId.length > 0 && typeof row.name === 'string' && row.name.length > 0;","tryCatchPattern":"try {\n  const rows = await ctrip.hotelSearch({ cityId, checkin, checkout });\n} catch (e) {\n  if (e instanceof CommandExecutionError && /hotelId\\/name anchors/.test(e.message)) {\n    // treat as schema drift: alert, update mapping, or fall back to another source\n  } else throw e;\n}","preventionTips":["Inspect a sample raw entry after each Ctrip release","Keep mapHotelRow field mappings in one easy-to-update place","Sanity-check hotel ids against a regex before use","Monitor for sudden zero-anchor rates as a schema-drift alarm"],"tags":["scraping","schema-drift","mapping","ctrip"],"backgroundTag":"schema-validation-failed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}