{"record":{"id":"dcd03d001e9af7d5","repo":"jackwener/OpenCLI","slug":"ctrip-flight-api-capture","errorCode":null,"errorMessage":"Ctrip flight API capture","messagePattern":"Ctrip flight API capture","errorType":"exception","errorClass":"TimeoutError","httpStatus":null,"severity":"error","filePath":"clis/ctrip/flight.js","lineNumber":196,"sourceCode":"            `https://flights.ctrip.com/online/list/oneway-${fromCode.toLowerCase()}-${toCode.toLowerCase()}` +\n            `?depdate=${date}&cabin=Y_S_C_F&adult=1&child=0&infant=0`;\n        if (typeof page?.startNetworkCapture !== 'function' ||\n            typeof page?.readNetworkCapture !== 'function' ||\n            !await page.startNetworkCapture(CAPTURE_PATTERN)) {\n            throw new CommandExecutionError('Ctrip flight requires browser response interception');\n        }\n        await page.readNetworkCapture();\n        await page.goto(searchUrl);\n        // The initial document can finish before the large batchSearch body.\n        // The first rendered card is only a readiness signal; row data still\n        // comes exclusively from the structured response below.\n        const readiness = await page.evaluate(WAIT_FOR_BATCH_CAPTURE_JS);\n        if (readiness === 'captcha') {\n            throw new AuthRequiredError('flights.ctrip.com', 'Ctrip is asking for a captcha; complete it in your browser session and retry');\n        }\n        const itineraries = parseBatchSearchCaptures(await page.readNetworkCapture());\n        if (!itineraries) {\n            throw new TimeoutError('Ctrip flight API capture', CAPTURE_TIMEOUT_SECONDS, 'No batchSearch response was observed after opening the results page.');\n        }\n        if (itineraries.length === 0) {\n            throw new EmptyResultError('ctrip flight', `No flights for ${fromCode}→${toCode} on ${date}`);\n        }\n        const rows = itineraries\n            .map((itinerary, index) => mapItinerary(itinerary, searchUrl, index))\n            // The page groups direct flights before transfers, then applies its\n            // displayed starting price and departure-time order inside each group.\n            .sort(([rowA, connectingA, departureA, idA], [rowB, connectingB, departureB, idB]) =>\n                Number(connectingA) - Number(connectingB) || rowA.price - rowB.price ||\n                departureA.localeCompare(departureB) || idA.localeCompare(idB))\n            .slice(0, limit)\n            .map(([row], index) => ({\n                rank: index + 1,\n                airline: row.airline,\n                flightNo: row.flightNo,\n                aircraft: row.aircraft,\n                departureTime: row.departureTime,","sourceCodeStart":178,"sourceCodeEnd":214,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/ctrip/flight.js#L178-L214","documentation":"A TimeoutError raised when no batchSearch network response was captured within CAPTURE_TIMEOUT_SECONDS (12s) after opening the results page. parseBatchSearchCaptures returns null when the capture log contains no entry whose URL includes '/international/search/api/search/batchSearch', and the command converts that into this timeout so callers know the structured API response never arrived.","triggerScenarios":"Ctrip never issues the batchSearch XHR (page stuck on a shell, JS errors, or the request URL pattern changed); the response arrives after the 12s window on a slow connection; startNetworkCapture missed the request because interception began late; the page navigated to a captcha/verify state without setting the 'captcha' readiness signal.","commonSituations":"Slow or throttled networks; regional Ctrip variants served under different API paths; heavy load days where Ctrip delays search; an A/B test replacing batchSearch with a new endpoint; anti-bot soft-block returning no data without an explicit captcha page.","solutions":["Retry — transient slowness or a stuck page is the most common cause.","Increase CAPTURE_TIMEOUT_SECONDS if your network routinely takes >12s for the large batchSearch payload.","Verify the capture pattern still matches by checking the Network tab for the search XHR; update CAPTURE_PATTERN if Ctrip changed the endpoint.","Confirm the page isn't being soft-blocked (no flight cards rendered either) and, if so, treat it as an anti-bot issue: use a trusted session/IP."],"exampleFix":"// before\nconst CAPTURE_TIMEOUT_SECONDS = 12;\n\n// after: tolerate slow networks\nconst CAPTURE_TIMEOUT_SECONDS = 30;","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  const rows = await ctripFlight({ from, to, date });\n} catch (e) {\n  if (e instanceof TimeoutError && e.message.includes('Ctrip flight API capture')) {\n    return retryWithBackoff(() => ctripFlight({ from, to, date }), { attempts: 2, baseMs: 3000 });\n  }\n  throw e;\n}","preventionTips":["Retry timeout runs once or twice before escalating — capture misses are often transient.","Raise CAPTURE_TIMEOUT_SECONDS on slow networks.","Verify CAPTURE_PATTERN still matches the live batchSearch endpoint after Ctrip deploys.","Check for soft anti-bot blocks (no captcha text, no data) and switch sessions/IPs when suspected."],"tags":["timeout","network","scraper","xhr-capture"],"backgroundTag":"response-capture-timeout","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}