jackwener/OpenCLI · error · CommandExecutionError

12306 returned an unexpected payload shape

Error message

12306 ${endpoint} returned an unexpected payload shape

What it means

The endpoint answered with parseable JSON, but the JSON was neither the rotation hint (`{c_url: ...}`) nor the expected train-list shape (`{data: {result: [...]}}`). This means 12306 responded 200 with valid JSON that does not match any known protocol message, so queryLeftTickets cannot extract train rows and throws CommandExecutionError.

Solutions

  1. Verify the query parameters are correct: from/to must be uppercase 3-4 letter telecodes and date must be a valid future YYYY-MM-DD (booking window).
  2. Retry after a delay; transient status-only envelopes during peak load often resolve.
  3. Re-mint the session (mintSession) and retry — expired cookies can yield JSON error envelopes.
  4. Log the full JSON body and compare with the current 12306 web client response; if the schema moved (e.g. data.result renamed), update the shape check in queryLeftTickets.
  5. As a workaround, try a different endpoint from QUERY_ENDPOINTS by testing manually.

Example fix

// before
if (Array.isArray(json?.data?.result)) {
    return json.data.result;
}
throw new CommandExecutionError(`12306 ${endpoint} returned an unexpected payload shape`);
// after
if (Array.isArray(json?.data?.result)) {
    return json.data.result;
}
if (Array.isArray(json?.messages) && json.messages.length) {
    throw new CommandExecutionError(`12306 ${endpoint} rejected the query: ${json.messages.join('; ')}`);
}
throw new CommandExecutionError(`12306 ${endpoint} returned an unexpected payload shape: ${JSON.stringify(json).slice(0, 200)}`);
Defensive patterns

Strategy: type-guard

Validate before calling

function looksLikeLeftTicketEnvelope(json) {
  return json && typeof json === 'object' &&
    (typeof json.c_url === 'string' || Array.isArray(json?.data?.result));
}

Type guard

function hasTrainResults(json) {
  return typeof json === 'object' && json !== null
    && Array.isArray(json.data)
    ? Array.isArray(json.data.result)
    : Array.isArray(json?.data?.result);
}

Try / catch

try {
  rows = await queryLeftTickets(cookie, from, to, date);
} catch (e) {
  if (/unexpected payload shape/.test(e.message)) {
    // log full JSON body and check the 12306 web client for a schema change
    console.error('12306 schema drift:', e.message);
  }
  throw e;
}

Prevention

When it happens

Trigger: A 200 JSON response from `otn/leftTicket/<endpoint>` whose body lacks both `c_url` and `data.result` — e.g. `{status:false, messages:[...]}` validation replies, an empty `{}` object, a response where `data` exists but `result` is missing/not an array, or a new protocol version that nests the train array elsewhere.

Common situations: 12306 deployed a schema change to the leftTicket API; the endpoint name happens to be valid but returns a status-only envelope for invalid query parameters (e.g. malformed date or telecode); server returns an error envelope with HTTP 200 during partial outages; reverse-proxy or cache serving a stale/incorrect JSON document.

Related errors


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

Appendix: source

Thrown at clis/12306/trains.js:98

            throw new CommandExecutionError(`12306 ${endpoint} returned HTTP ${resp.status}`);
        }
        const text = await resp.text();
        lastResponseText = text;
        let json;
        try { json = JSON.parse(text); } catch {
            throw new CommandExecutionError(`12306 ${endpoint} returned non-JSON body`);
        }
        if (json?.c_url && typeof json.c_url === 'string') {
            const rotated = await parseRotationEndpoint(resp, endpoint, text);
            if (rotated && !tried.has(rotated)) {
                queue.unshift(rotated);
            }
            continue;
        }
        if (Array.isArray(json?.data?.result)) {
            return json.data.result;
        }
        throw new CommandExecutionError(`12306 ${endpoint} returned an unexpected payload shape`);
    }
    throw new CommandExecutionError(`12306 rejected every known query endpoint name (${QUERY_ENDPOINTS.join(', ')}); the wire protocol may have changed. Last body: ${lastResponseText.slice(0, 200)}`);
}

cli({
    site: '12306',
    name: 'trains',
    access: 'read',
    description: 'List trains between two 12306 stations on a given date (anonymous, no login required)',
    domain: 'kyfw.12306.cn',
    strategy: Strategy.PUBLIC,
    browser: false,
    args: [
        { name: 'from', positional: true, required: true, help: 'Origin station: Chinese name (北京), telecode (BJP), or pinyin (beijing)' },
        { name: 'to', positional: true, required: true, help: 'Destination station: same forms as <from>' },
        { name: 'date', required: true, help: 'Departure date in YYYY-MM-DD' },
        { name: 'limit', type: 'int', default: 50, help: `Maximum rows (1-${MAX_LIMIT})` },
    ],

View on GitHub (pinned to 49907e53dc)