jackwener/OpenCLI · error · CommandExecutionError
12306 ${endpoint} 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.
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)
Solutions
- 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).
- Retry after a delay; transient status-only envelopes during peak load often resolve.
- Re-mint the session (mintSession) and retry — expired cookies can yield JSON error envelopes.
- 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.
- 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
- Validate query params (telecodes, valid future date) before calling.
- Keep the library updated; 12306 occasionally changes response envelopes.
- On failure, capture the full JSON to diff against the live web client's payload.
- Re-mint cookies on repeated shape errors — expired sessions can yield error envelopes.
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
- 12306 rejected every known query endpoint name (${QUERY_ENDP
- Failed to parse 12306 station_name.js: no station records fo
- ${label} returned an unexpected payload shape; expected an o
- archive search returned malformed JSON: ${error?.message ||
- archive snapshots returned malformed JSON: ${error?.message
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/f64832df9fa1111f.
Report an issue: GitHub.