{"record":{"id":"7f507fd8bc8f79c0","repo":"jackwener/OpenCLI","slug":"ctrip-flight-api-returned-a-malformed-batchsearch","errorCode":null,"errorMessage":"Ctrip flight API returned a malformed batchSearch payload","messagePattern":"Ctrip flight API returned a malformed batchSearch payload","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/ctrip/flight.js","lineNumber":92,"sourceCode":"        if (entry?.responseBodyTruncated === true) {\n            throw new CommandExecutionError('Ctrip flight API response exceeded the browser capture limit');\n        }\n        if (typeof entry?.responsePreview !== 'string') {\n            throw new CommandExecutionError('Ctrip flight API response body was unavailable');\n        }\n        let payload;\n        try {\n            payload = JSON.parse(entry.responsePreview);\n        }\n        catch {\n            throw new CommandExecutionError('Ctrip flight API returned invalid JSON');\n        }\n        if (payload?.status !== 0) {\n            throw new CommandExecutionError(`Ctrip flight API failed (status=${String(payload?.status)}): ${cleanString(payload?.msg) || 'unknown error'}`);\n        }\n        const itineraries = payload?.data?.flightItineraryList;\n        if (!Array.isArray(itineraries) || typeof payload?.data?.context?.finished !== 'boolean') {\n            throw new CommandExecutionError('Ctrip flight API returned a malformed batchSearch payload');\n        }\n        for (const itinerary of itineraries) {\n            const id = cleanString(itinerary?.itineraryId);\n            if (!id) throw new CommandExecutionError('Ctrip flight API returned an itinerary without an id');\n            byId.set(id, itinerary);\n        }\n        finished = payload.data.context.finished;\n    }\n    if (!finished) {\n        throw new CommandExecutionError('Ctrip flight batchSearch ended before the upstream search reported completion');\n    }\n    return [...byId.values()];\n}\n\nfunction mapItinerary(itinerary, searchUrl, index) {\n    const segments = itinerary?.flightSegments;\n    const prices = itinerary?.priceList;\n    if (!Array.isArray(segments) || segments.length === 0 || !Array.isArray(prices) || prices.length === 0) {","sourceCodeStart":74,"sourceCodeEnd":110,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/ctrip/flight.js#L74-L110","documentation":"Thrown when a captured batchSearch response has status 0 but its body does not match the expected schema: `payload.data.flightItineraryList` is not an array, or `payload.data.context.finished` is not a boolean. The library treats this as a contract violation — Ctrip returned a 'successful' payload whose shape the parser cannot trust.","triggerScenarios":"Ctrip returns status=0 with missing/renamed data fields (data absent, flightItineraryList renamed or null, context or context.finished missing/renamed), typically after an unannounced Ctrip API schema change or an A/B-test variant response.","commonSituations":"Developers encounter this when Ctrip ships a new response schema version, when the page requests a different endpoint variant than the captured pattern expects, or when regional Ctrip sites return localized/altered payload shapes.","solutions":["Log the full captured responsePreview for the failing entry to see the actual payload shape.","Diff the observed payload against the expected schema (data.flightItineraryList, data.context.finished) to identify renamed or moved fields.","Update the parsing in parseBatchSearchCaptures (clis/ctrip/flight.js:90-93) to match the new schema.","If Ctrip is A/B testing, pin a stable site variant or handle both payload shapes.","Retry later if it looks like a transient partial/degraded response from Ctrip."],"exampleFix":"// before: trusting the shape blindly\nconst itineraries = payload?.data?.flightItineraryList;\n// after: defensively inspect before calling the library path\nconst itineraries = payload?.data?.flightItineraryList;\nif (payload?.status !== 0 || !Array.isArray(itineraries)) {\n  console.error('unexpected payload:', entry.responsePreview.slice(0, 500));\n}","handlingStrategy":"type-guard","validationCode":"// after capture, before trusting results\nconst payload = JSON.parse(entry.responsePreview);\nconst shaped = typeof payload?.status === 'number'\n  && Array.isArray(payload?.data?.flightItineraryList)\n  && typeof payload?.data?.context?.finished === 'boolean';","typeGuard":"function hasBatchSearchShape(payload) {\n  return payload != null\n    && typeof payload === 'object'\n    && payload.status === 0\n    && Array.isArray(payload?.data?.flightItineraryList)\n    && typeof payload?.data?.context?.finished === 'boolean';\n}","tryCatchPattern":"try {\n  const rows = await cli.run(['ctrip', 'flight', from, to, date]);\n} catch (err) {\n  if (String(err.message).includes('malformed batchSearch payload')) {\n    console.error('Ctrip schema changed; capture and inspect the raw responsePreview');\n  } else throw err;\n}","preventionTips":["Pin/monitor the Ctrip site version you automate against","Keep the capture pattern and parser in sync with the current batchSearch schema","Alert on this error so schema changes are detected early","Persist raw responsePreview samples for regression testing the parser"],"tags":["schema","api","third-party","parsing"],"backgroundTag":"schema-validation-failed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}