{"record":{"id":"5bfec01b9788106d","repo":"jackwener/OpenCLI","slug":"12306-querybytrainno-returned-non-json-body-5bfec0","errorCode":null,"errorMessage":"12306 queryByTrainNo returned non-JSON body","messagePattern":"12306 queryByTrainNo returned non-JSON body","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/12306/train.js","lineNumber":31,"sourceCode":"const TRAIN_NO_RE = /^[0-9A-Za-z]{8,18}$/;\n\nasync function queryStops(cookieHeader, trainNo, fromCode, toCode, date, fetchImpl = fetch) {\n    const url = `https://kyfw.12306.cn/otn/czxx/queryByTrainNo?train_no=${trainNo}&from_station_telecode=${fromCode}&to_station_telecode=${toCode}&depart_date=${date}`;\n    const resp = await fetchImpl(url, {\n        headers: {\n            'User-Agent': UA,\n            'Referer': 'https://kyfw.12306.cn/otn/leftTicket/init',\n            'Cookie': cookieHeader,\n        },\n    });\n    if (!resp.ok) {\n        throw new CommandExecutionError(`12306 queryByTrainNo returned HTTP ${resp.status}`);\n    }\n    let json;\n    try {\n        json = await resp.json();\n    } catch {\n        throw new CommandExecutionError('12306 queryByTrainNo returned non-JSON body');\n    }\n    if (json?.status !== true || !Array.isArray(json?.data?.data)) {\n        throw new CommandExecutionError(`12306 queryByTrainNo returned an unexpected payload shape`);\n    }\n    return json.data.data;\n}\n\ncli({\n    site: '12306',\n    name: 'train',\n    access: 'read',\n    description: 'List every station a 12306 train calls at, with arrival / departure / stopover time (anonymous, no login required)',\n    domain: 'kyfw.12306.cn',\n    strategy: Strategy.PUBLIC,\n    browser: false,\n    args: [\n        { name: 'train-no', positional: true, required: true, help: 'Internal train_no from `12306 trains` (e.g. 24000000G10L), not the public code (G1)' },\n        { name: 'from', required: true, help: 'Origin station for the segment: Chinese name, telecode, or pinyin' },","sourceCodeStart":13,"sourceCodeEnd":49,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/12306/train.js#L13-L49","documentation":"After a successful HTTP status, queryStops calls resp.json() on the queryByTrainNo response. When the body is not valid JSON (HTML login/verification page, anti-bot interstitial, gzip garbage, or empty body), JSON parsing throws and the library converts it to this CommandExecutionError. It means 12306 responded but with something other than the expected JSON API payload.","triggerScenarios":"12306 returning an HTML CAPTCHA/login page instead of JSON (anti-bot triggered); a proxy or captive portal injecting an HTML error page; response body truncated or empty despite 200 OK; wrong Content-Type with non-JSON body.","commonSituations":"Scraping too aggressively triggers 12306's dynamic JS verification page; requests through a hotel/office captive portal; expired session redirecting (with 200) to an HTML page; middleboxes mangling compression.","solutions":["Retry after a delay — anti-bot HTML interstitials often disappear once rate pressure drops.","Re-mint session cookies and retry; an invalid session can yield an HTML page with 200.","Slow down request rate and reuse one session across queries instead of hammering the endpoint.","If it persists, verify the network path isn't injecting HTML (captive portal, corporate proxy)."],"exampleFix":"// before (assume JSON always)\nconst data = await stops(trainNo);\n// after\nlet data;\ntry { data = await stops(trainNo); }\ncatch (e) {\n  if (/non-JSON body|unexpected payload shape/.test(e.message)) { await sleep(3000); data = await stops(trainNo); }\n  else throw e;\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"let data;\ntry {\n  data = await stops(trainNo);\n} catch (e) {\n  if (/non-JSON body/.test(e.message)) {\n    await new Promise((r) => setTimeout(r, 2000)); // anti-bot interstitial usually clears\n    data = await stops(trainNo);\n  } else throw e;\n}","preventionTips":["Rate-limit requests to kyfw.12306.cn — HTML anti-bot pages are the usual non-JSON source.","Reuse a freshly minted session instead of replaying stale cookies.","Catch this error specifically and retry once after a delay before giving up.","Verify no proxy/captive portal is injecting HTML into responses."],"tags":["network","http","json-parsing","upstream-api","anti-bot"],"backgroundTag":"invalid-json-response","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}