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

  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.