{"record":{"id":"f8fcbf2d92667080","repo":"jackwener/OpenCLI","slug":"ctrip-flight-batchsearch-ended-before-the-upstream","errorCode":null,"errorMessage":"Ctrip flight batchSearch ended before the upstream search reported completion","messagePattern":"Ctrip flight batchSearch ended before the upstream search reported completion","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/ctrip/flight.js","lineNumber":102,"sourceCode":"        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];\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);","sourceCodeStart":84,"sourceCodeEnd":120,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/ctrip/flight.js#L84-L120","documentation":"Thrown after all captured batchSearch entries are parsed when the last observed response's data.context.finished flag is false. Ctrip's batchSearch is paginated/streamed: the page keeps polling until finished===true. If the capture window ends before upstream reports completion, the results may be incomplete, so the library refuses to return partial data.","triggerScenarios":"All captured batchSearch responses parsed fine but none (or not the last) had context.finished===true — the capture timeout (CAPTURE_TIMEOUT_SECONDS=12s) elapsed, the browser page was closed/navigated early, or Ctrip is polling slowly for a high-demand route/date.","commonSituations":"Developers hit this on popular routes where Ctrip takes longer than the capture window, on slow networks, or when the automation harness closes the page or moves on before polling completes.","solutions":["Increase CAPTURE_TIMEOUT_SECONDS / the surrounding wait timeout to give Ctrip's polling loop more time.","Retry the search — slow polls are often transient.","Reduce result pressure (smaller limit, off-peak times) so Ctrip finishes the search faster.","Check network health / browser stability; ensure the page is not closed or navigated during capture.","If Ctrip changed the finished flag location, update the parsing in parseBatchSearchCaptures."],"exampleFix":"// before\nconst CAPTURE_TIMEOUT_SECONDS = 12;\n// after\nconst CAPTURE_TIMEOUT_SECONDS = 30;","handlingStrategy":"retry","validationCode":"// not pre-validatable; ensure the page stays open and budget enough wait time\n// confirm the browser context is alive before starting the search\nif (browserContext.isClosed()) throw new Error('browser context closed; reopen before searching');","typeGuard":"function reportedFinished(payload) {\n  return payload?.data?.context?.finished === true;\n}","tryCatchPattern":"try {\n  const rows = await cli.run(['ctrip', 'flight', from, to, date]);\n} catch (err) {\n  if (String(err.message).includes('ended before the upstream search reported completion')) {\n    await sleep(3000); // give Ctrip polling more time, then retry\n    const rows = await cli.run(['ctrip', 'flight', from, to, date]);\n  } else throw err;\n}","preventionTips":["Raise CAPTURE_TIMEOUT_SECONDS for high-demand routes","Never close or navigate the captured page while the search runs","Retry with backoff; Ctrip poll duration varies with load","Run during off-peak hours for faster upstream completion"],"tags":["timeout","pagination","network","third-party"],"backgroundTag":"incomplete-paginated-response","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}