jackwener/OpenCLI · error · CommandExecutionError
Failed to parse 12306 station_name.js: source string not fou
Error message
Failed to parse 12306 station_name.js: source string not found
What it means
CommandExecutionError from parseStationBundle when the downloaded station_name.js text contains no single-quoted string literal. The parser expects the bundle in the form `var station_names ='@...|...@...';` and regex-extracts the first quoted string; if none is found the fetched resource is not the expected script, so parsing aborts.
Source
Thrown at clis/12306/utils.js:44
*
* Bundle format (single line, `@`-delimited records, each `|`-delimited):
* `var station_names ='@bjb|北京北|VAP|beijingbei|bjb|0|0357|北京|||...';`
*
* Per-record fields (positional):
* [0] short pinyin alias (e.g. `bjb`)
* [1] Chinese station name (e.g. `北京北`)
* [2] telecode (3-4 uppercase letters, e.g. `VAP`) - this is the
* wire format 12306 uses for `from_station` / `to_station`.
* [3] full pinyin (e.g. `beijingbei`)
* [4] short alias (duplicate of [0] usually)
* [5] index/rank
* [6] city code
* [7] city name (e.g. `北京`)
*/
export function parseStationBundle(text) {
const match = text.match(/'([^']+)'/);
if (!match) {
throw new CommandExecutionError('Failed to parse 12306 station_name.js: source string not found');
}
const raw = match[1];
const records = raw.split('@').filter(Boolean);
const stations = [];
for (const r of records) {
const parts = r.split('|');
if (parts.length < 8 || !parts[2]) continue;
stations.push({
short: parts[0] || '',
name: parts[1] || '',
code: parts[2] || '',
pinyin: parts[3] || '',
abbr: parts[4] || '',
city: parts[7] || '',
});
}
if (stations.length === 0) {
throw new CommandExecutionError('Failed to parse 12306 station_name.js: no station records found');View on GitHub (pinned to 49907e53dc)
Solutions
- Re-run after a delay and from a non-datacenter network — the anti-bot HTML usually disappears.
- Log/inspect the beginning of the fetched text to see what was actually returned (HTML? JSON? empty?).
- Verify the station bundle URL (https://kyfw.12306.cn/otn/resources/js/framework/station_name.js) still serves the JS — if 12306 moved it, update STATION_BUNDLE_URL in clis/12306/utils.js.
- Cache a copy of the station bundle locally and fall back to it when the live fetch is unusable.
Example fix
// before
return parseStationBundle(await resp.text());
// after
const text = await resp.text();
if (/<html/i.test(text)) {
throw new CommandExecutionError('station_name.js fetch got an HTML page (anti-bot or moved resource); check STATION_BUNDLE_URL');
}
return parseStationBundle(text); Defensive patterns
Strategy: fallback
Validate before calling
const text = await (await fetch(STATION_BUNDLE_URL, { headers: { 'User-Agent': UA } })).text();
if (!text.includes("var station_names")) {
console.warn('station_name.js body is not the expected JS — WAF page or moved resource');
} Type guard
function isStationBundleScript(text) {
return typeof text === 'string' && /var\s+station_names\s*=\s*'/.test(text);
} Try / catch
try {
const stations = await fetchStationBundle();
} catch (e) {
if (/source string not found/.test(e.message)) {
// serve a cached copy while the live fetch is blocked/changed
const stations = parseStationBundle(await fs.readFile('./station_name.cache.js', 'utf8'));
} else throw e;
} Prevention
- Cache the station bundle locally and refresh on success.
- Detect HTML responses (anti-bot/WAF) before parsing and back off.
- Verify the STATION_BUNDLE_URL still resolves to the JS resource after 12306 site updates.
- Run from residential networks; datacenter IPs frequently get HTML challenge pages.
When it happens
Trigger: fetchStationBundle got HTTP 200 but the body is HTML (anti-bot/WAF page, error page, maintenance notice) or some other non-JS content with no `'...'` literal — e.g. a login redirect page or a JSON error document from kyfw.12306.cn.
Common situations: 12306 WAF serving an HTML challenge to datacenter IPs; 12306 moved or renamed station_name.js so the URL now serves a 200 error page; offline dev proxy returning a captive-portal page; 12306 down for maintenance returning HTML with status 200.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Ctrip flight API returned invalid JSON
- Reuters search returned a non-JSON body${detail}
- toutiao recommend returned malformed JSON: ${error?.message
- PARSE_ERROR
- Zhihu answer detail returned malformed JSON: ${data.__malfor
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/0ea876de8b516223.
Report an issue: GitHub.