{"record":{"id":"ce7d1e5d81a35480","repo":"jackwener/OpenCLI","slug":"ctrip-flight-api-returned-an-itinerary-without-an","errorCode":null,"errorMessage":"Ctrip flight API returned an itinerary without an id","messagePattern":"Ctrip flight API returned an itinerary without an id","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/ctrip/flight.js","lineNumber":96,"sourceCode":"            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) {\n        throw new CommandExecutionError(`Ctrip flight API returned malformed itinerary at index ${index}`);\n    }\n    const legs = segments.flatMap((segment) => Array.isArray(segment?.flightList) ? segment.flightList : []);\n    const first = legs[0];","sourceCodeStart":78,"sourceCodeEnd":114,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/ctrip/flight.js#L78-L114","documentation":"Thrown when an itinerary inside payload.data.flightItineraryList has no non-empty itineraryId after trimming. The library deduplicates itineraries in a Map keyed by itineraryId, so an item without an id cannot be processed and is treated as a malformed record.","triggerScenarios":"A captured batchSearch response (status=0, valid array shape) contains at least one itinerary object where itineraryId is missing, null, empty string, or whitespace-only — often a placeholder/degraded entry Ctrip emits for sold-out or unpriceable flights.","commonSituations":"Seen when Ctrip includes ghost/placeholder itineraries in results, during schema changes that rename itineraryId, or when heavy automation traffic causes Ctrip to degrade some result entries.","solutions":["Dump responsePreview and inspect the offending itinerary object to confirm which field now holds the id.","Retry the search — placeholder entries are often transient; a fresh query may return clean records.","Update parsing to read the new id field name if Ctrip renamed itineraryId.","As a workaround, filter out id-less itineraries before processing if you control the calling layer upstream of this parser."],"exampleFix":"// before\nfor (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// after (tolerate id-less placeholder entries)\nfor (const itinerary of itineraries) {\n    const id = cleanString(itinerary?.itineraryId) || cleanString(itinerary?.itineraryID);\n    if (!id) continue; // skip placeholder entries instead of failing the whole search","handlingStrategy":"validation","validationCode":"// validate each itinerary has an id before processing\nconst valid = (payload?.data?.flightItineraryList || []).every(\n  (it) => typeof it?.itineraryId === 'string' && it.itineraryId.trim() !== ''\n);\nif (!valid) console.warn('response contains id-less itineraries; expect this error');","typeGuard":"function hasItineraryId(itinerary) {\n  return typeof itinerary?.itineraryId === 'string' && itinerary.itineraryId.trim().length > 0;\n}","tryCatchPattern":"try {\n  const rows = await cli.run(['ctrip', 'flight', from, to, date]);\n} catch (err) {\n  if (String(err.message).includes('itinerary without an id')) {\n    console.error('Ctrip returned a placeholder itinerary; retry the search');\n  } else throw err;\n}","preventionTips":["Retry once automatically — id-less entries are often transient placeholders","Keep the parser updated if Ctrip renames itineraryId","Prefer smaller limit values to reduce degraded result entries","Log raw payloads when this fires to identify the new id field"],"tags":["schema","api","third-party","data-quality"],"backgroundTag":"schema-validation-failed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}