jackwener/OpenCLI · error · CommandExecutionError

Ctrip flight batchSearch ended before the upstream search re

Error message

Ctrip flight batchSearch ended before the upstream search reported completion

What it means

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.

Source

Thrown at clis/ctrip/flight.js:102

        catch {
            throw new CommandExecutionError('Ctrip flight API returned invalid JSON');
        }
        if (payload?.status !== 0) {
            throw new CommandExecutionError(`Ctrip flight API failed (status=${String(payload?.status)}): ${cleanString(payload?.msg) || 'unknown error'}`);
        }
        const itineraries = payload?.data?.flightItineraryList;
        if (!Array.isArray(itineraries) || typeof payload?.data?.context?.finished !== 'boolean') {
            throw new CommandExecutionError('Ctrip flight API returned a malformed batchSearch payload');
        }
        for (const itinerary of itineraries) {
            const id = cleanString(itinerary?.itineraryId);
            if (!id) throw new CommandExecutionError('Ctrip flight API returned an itinerary without an id');
            byId.set(id, itinerary);
        }
        finished = payload.data.context.finished;
    }
    if (!finished) {
        throw new CommandExecutionError('Ctrip flight batchSearch ended before the upstream search reported completion');
    }
    return [...byId.values()];
}

function mapItinerary(itinerary, searchUrl, index) {
    const segments = itinerary?.flightSegments;
    const prices = itinerary?.priceList;
    if (!Array.isArray(segments) || segments.length === 0 || !Array.isArray(prices) || prices.length === 0) {
        throw new CommandExecutionError(`Ctrip flight API returned malformed itinerary at index ${index}`);
    }
    const legs = segments.flatMap((segment) => Array.isArray(segment?.flightList) ? segment.flightList : []);
    const first = legs[0];
    const last = legs.at(-1);
    const airline = [...new Set(segments.map((segment) => cleanString(segment?.airlineName)).filter(Boolean))].join(' / ');
    const flightNo = [...new Set(legs.map((leg) => cleanString(leg?.flightNo)).filter(Boolean))].join(' / ');
    const aircraft = [...new Set(legs.map((leg) => cleanString(leg?.aircraftName)).filter(Boolean))].join(' / ') || null;
    const departureTime = timePart(first?.departureDateTime);
    const arrivalTime = timePart(last?.arrivalDateTime);

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Increase CAPTURE_TIMEOUT_SECONDS / the surrounding wait timeout to give Ctrip's polling loop more time.
  2. Retry the search — slow polls are often transient.
  3. Reduce result pressure (smaller limit, off-peak times) so Ctrip finishes the search faster.
  4. Check network health / browser stability; ensure the page is not closed or navigated during capture.
  5. If Ctrip changed the finished flag location, update the parsing in parseBatchSearchCaptures.

Example fix

// before
const CAPTURE_TIMEOUT_SECONDS = 12;
// after
const CAPTURE_TIMEOUT_SECONDS = 30;
Defensive patterns

Strategy: retry

Validate before calling

// not pre-validatable; ensure the page stays open and budget enough wait time
// confirm the browser context is alive before starting the search
if (browserContext.isClosed()) throw new Error('browser context closed; reopen before searching');

Type guard

function reportedFinished(payload) {
  return payload?.data?.context?.finished === true;
}

Try / catch

try {
  const rows = await cli.run(['ctrip', 'flight', from, to, date]);
} catch (err) {
  if (String(err.message).includes('ended before the upstream search reported completion')) {
    await sleep(3000); // give Ctrip polling more time, then retry
    const rows = await cli.run(['ctrip', 'flight', from, to, date]);
  } else throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/f8fcbf2d92667080. Report an issue: GitHub.