{"record":{"id":"f64832df9fa1111f","repo":"jackwener/OpenCLI","slug":"12306-endpoint-returned-an-unexpected-payload-s","errorCode":null,"errorMessage":"12306 ${endpoint} returned an unexpected payload shape","messagePattern":"12306 (.+?) returned an unexpected payload shape","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/12306/trains.js","lineNumber":98,"sourceCode":"            throw new CommandExecutionError(`12306 ${endpoint} returned HTTP ${resp.status}`);\n        }\n        const text = await resp.text();\n        lastResponseText = text;\n        let json;\n        try { json = JSON.parse(text); } catch {\n            throw new CommandExecutionError(`12306 ${endpoint} returned non-JSON body`);\n        }\n        if (json?.c_url && typeof json.c_url === 'string') {\n            const rotated = await parseRotationEndpoint(resp, endpoint, text);\n            if (rotated && !tried.has(rotated)) {\n                queue.unshift(rotated);\n            }\n            continue;\n        }\n        if (Array.isArray(json?.data?.result)) {\n            return json.data.result;\n        }\n        throw new CommandExecutionError(`12306 ${endpoint} returned an unexpected payload shape`);\n    }\n    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)}`);\n}\n\ncli({\n    site: '12306',\n    name: 'trains',\n    access: 'read',\n    description: 'List trains between two 12306 stations on a given date (anonymous, no login required)',\n    domain: 'kyfw.12306.cn',\n    strategy: Strategy.PUBLIC,\n    browser: false,\n    args: [\n        { name: 'from', positional: true, required: true, help: 'Origin station: Chinese name (北京), telecode (BJP), or pinyin (beijing)' },\n        { name: 'to', positional: true, required: true, help: 'Destination station: same forms as <from>' },\n        { name: 'date', required: true, help: 'Departure date in YYYY-MM-DD' },\n        { name: 'limit', type: 'int', default: 50, help: `Maximum rows (1-${MAX_LIMIT})` },\n    ],","sourceCodeStart":80,"sourceCodeEnd":116,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/12306/trains.js#L80-L116","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nif (Array.isArray(json?.data?.result)) {\n    return json.data.result;\n}\nthrow new CommandExecutionError(`12306 ${endpoint} returned an unexpected payload shape`);\n// after\nif (Array.isArray(json?.data?.result)) {\n    return json.data.result;\n}\nif (Array.isArray(json?.messages) && json.messages.length) {\n    throw new CommandExecutionError(`12306 ${endpoint} rejected the query: ${json.messages.join('; ')}`);\n}\nthrow new CommandExecutionError(`12306 ${endpoint} returned an unexpected payload shape: ${JSON.stringify(json).slice(0, 200)}`);","handlingStrategy":"type-guard","validationCode":"function looksLikeLeftTicketEnvelope(json) {\n  return json && typeof json === 'object' &&\n    (typeof json.c_url === 'string' || Array.isArray(json?.data?.result));\n}","typeGuard":"function hasTrainResults(json) {\n  return typeof json === 'object' && json !== null\n    && Array.isArray(json.data)\n    ? Array.isArray(json.data.result)\n    : Array.isArray(json?.data?.result);\n}","tryCatchPattern":"try {\n  rows = await queryLeftTickets(cookie, from, to, date);\n} catch (e) {\n  if (/unexpected payload shape/.test(e.message)) {\n    // log full JSON body and check the 12306 web client for a schema change\n    console.error('12306 schema drift:', e.message);\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["network","json","schema","api-change"],"backgroundTag":"unexpected-api-payload-shape","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}