jackwener/OpenCLI · error · CommandExecutionError
12306 queryByTrainNo returned non-JSON body
Error message
12306 queryByTrainNo returned non-JSON body
What it means
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.
Source
Thrown at clis/12306/train.js:31
const TRAIN_NO_RE = /^[0-9A-Za-z]{8,18}$/;
async function queryStops(cookieHeader, trainNo, fromCode, toCode, date, fetchImpl = fetch) {
const url = `https://kyfw.12306.cn/otn/czxx/queryByTrainNo?train_no=${trainNo}&from_station_telecode=${fromCode}&to_station_telecode=${toCode}&depart_date=${date}`;
const resp = await fetchImpl(url, {
headers: {
'User-Agent': UA,
'Referer': 'https://kyfw.12306.cn/otn/leftTicket/init',
'Cookie': cookieHeader,
},
});
if (!resp.ok) {
throw new CommandExecutionError(`12306 queryByTrainNo returned HTTP ${resp.status}`);
}
let json;
try {
json = await resp.json();
} catch {
throw new CommandExecutionError('12306 queryByTrainNo returned non-JSON body');
}
if (json?.status !== true || !Array.isArray(json?.data?.data)) {
throw new CommandExecutionError(`12306 queryByTrainNo returned an unexpected payload shape`);
}
return json.data.data;
}
cli({
site: '12306',
name: 'train',
access: 'read',
description: 'List every station a 12306 train calls at, with arrival / departure / stopover time (anonymous, no login required)',
domain: 'kyfw.12306.cn',
strategy: Strategy.PUBLIC,
browser: false,
args: [
{ name: 'train-no', positional: true, required: true, help: 'Internal train_no from `12306 trains` (e.g. 24000000G10L), not the public code (G1)' },
{ name: 'from', required: true, help: 'Origin station for the segment: Chinese name, telecode, or pinyin' },View on GitHub (pinned to 49907e53dc)
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).
Example fix
// before (assume JSON always)
const data = await stops(trainNo);
// after
let data;
try { data = await stops(trainNo); }
catch (e) {
if (/non-JSON body|unexpected payload shape/.test(e.message)) { await sleep(3000); data = await stops(trainNo); }
else throw e;
} Defensive patterns
Strategy: retry
Try / catch
let data;
try {
data = await stops(trainNo);
} catch (e) {
if (/non-JSON body/.test(e.message)) {
await new Promise((r) => setTimeout(r, 2000)); // anti-bot interstitial usually clears
data = await stops(trainNo);
} else throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — this error's family across 28 libraries.
Related errors
- 12306 ${endpoint} returned non-JSON body
- 12306 queryByTrainNo returned HTTP ${resp.status}
- coingecko derivatives returned malformed JSON: ${err?.messag
- coingecko global returned malformed JSON: ${err?.message ??
- coingecko returned malformed JSON: ${error?.message || error
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/5bfec01b9788106d.
Report an issue: GitHub.