{"record":{"id":"9663998f1fe2b0dc","repo":"jackwener/OpenCLI","slug":"ctrip-flight-api-returned-malformed-itinerary-at-i","errorCode":null,"errorMessage":"Ctrip flight API returned malformed itinerary at index ${index}","messagePattern":"Ctrip flight API returned malformed itinerary at index (.+?)","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/ctrip/flight.js","lineNumber":111,"sourceCode":"        }\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];\n    const last = legs.at(-1);\n    const airline = [...new Set(segments.map((segment) => cleanString(segment?.airlineName)).filter(Boolean))].join(' / ');\n    const flightNo = [...new Set(legs.map((leg) => cleanString(leg?.flightNo)).filter(Boolean))].join(' / ');\n    const aircraft = [...new Set(legs.map((leg) => cleanString(leg?.aircraftName)).filter(Boolean))].join(' / ') || null;\n    const departureTime = timePart(first?.departureDateTime);\n    const arrivalTime = timePart(last?.arrivalDateTime);\n    const departureAirport = cleanString(first?.departureAirportName);\n    const arrivalAirport = cleanString(last?.arrivalAirportName);\n    const price = Number(prices[0]?.sortPrice ?? prices[0]?.adultPrice);\n    if (!airline || !flightNo || !departureTime || !arrivalTime || !departureAirport || !arrivalAirport || !Number.isFinite(price)) {\n        throw new CommandExecutionError(`Ctrip flight API returned malformed itinerary at index ${index}`);\n    }\n    const row = {\n        airline,\n        flightNo,","sourceCodeStart":93,"sourceCodeEnd":129,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/ctrip/flight.js#L93-L129","documentation":"Thrown by mapItinerary when an itinerary lacks the minimum data needed to render a row: flightSegments is not a non-empty array, priceList is not a non-empty array, or any required display field (airline, flightNo, departure/arrival time, airport names, numeric price) is missing after mapping. Each itinerary from the API is expected to contain at least one segment and one price; anything else is treated as malformed.","triggerScenarios":"A deduplicated itinerary has empty/missing flightSegments or priceList, or its nested flightList legs lack flightNo/airlineName/departureDateTime/arrivalDateTime/airport names, or prices[0] lacks both sortPrice and adultPrice (so price is NaN).","commonSituations":"Occurs with Ctrip schema changes that rename fields (e.g. sortPrice/adultPrice), sold-out itineraries with empty priceList, charter/placeholder entries with no flight legs, or locale-dependent date-time formats that timePart cannot parse.","solutions":["Log the offending itinerary object at the given index to see which field is missing or renamed.","Check whether Ctrip renamed flightSegments/priceList/flightNo/sortPrice/adultPrice and update mapItinerary accordingly.","Retry the search — sold-out or placeholder itineraries are often transient.","Make parsing tolerant: skip instead of throwing when a single itinerary is malformed, if partial results are acceptable.","Verify date-time formats still match the regex in timePart if departure/arrival times come back empty."],"exampleFix":"// before\nconst price = Number(prices[0]?.sortPrice ?? prices[0]?.adultPrice);\n// after (fall back to any available price field)\nconst p = prices[0] || {};\nconst price = Number(p.sortPrice ?? p.adultPrice ?? p.price ?? p.minPrice);\nif (!Number.isFinite(price)) {\n  console.warn('skipping malformed itinerary', index);\n  return null;\n}","handlingStrategy":"validation","validationCode":"// validate itinerary essentials before expecting clean rows\nconst ok = (it) => Array.isArray(it?.flightSegments) && it.flightSegments.length > 0\n  && Array.isArray(it?.priceList) && it.priceList.length > 0\n  && Number.isFinite(Number(it.priceList[0]?.sortPrice ?? it.priceList[0]?.adultPrice));\nconst bad = (payload?.data?.flightItineraryList || []).filter((it) => !ok(it));\nif (bad.length) console.warn(`${bad.length} malformed itineraries expected to be rejected`);","typeGuard":"function isRenderableItinerary(itinerary) {\n  return Array.isArray(itinerary?.flightSegments) && itinerary.flightSegments.length > 0\n    && Array.isArray(itinerary?.priceList) && itinerary.priceList.length > 0\n    && Number.isFinite(Number(itinerary.priceList[0]?.sortPrice ?? itinerary.priceList[0]?.adultPrice));\n}","tryCatchPattern":"try {\n  const rows = await cli.run(['ctrip', 'flight', from, to, date]);\n} catch (err) {\n  const m = String(err.message).match(/malformed itinerary at index (\\d+)/);\n  if (m) console.error(`itinerary #${m[1]} lacks segments/prices; inspect raw payload or retry`);\n  else throw err;\n}","preventionTips":["Log the failing itinerary index and raw object when this fires","Track Ctrip field renames (flightSegments, priceList, sortPrice, adultPrice)","Prefer skipping bad rows over failing the whole search when partial results suffice","Verify locale/date-time formats still match timePart's regex after locale changes"],"tags":["schema","parsing","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"}