sansan0/TrendRadar · error · FileParseError

FILE_PARSE_ERROR

FILE_PARSE_ERROR

Error message

解析文件 {config_path} 失败: 配置文件不存在

What it means

Raised inside RSSParser._parse_json_feed when the content was detected as a JSON Feed (by _is_json_feed) but json.loads fails. Note the detection only sniffs JSON shape, so content that looks JSON-ish (starts like an object) yet contains syntax errors reaches this decoder and raises ValueError wrapping the JSONDecodeError with position info.

Source

Thrown at mcp_server/services/parser_service.py:367

        """
        解析YAML配置文件

        Args:
            config_path: 配置文件路径,默认为 config/config.yaml

        Returns:
            配置字典

        Raises:
            FileParseError: 配置文件解析错误
        """
        if config_path is None:
            config_path = self.project_root / "config" / "config.yaml"
        else:
            config_path = Path(config_path)

        if not config_path.exists():
            raise FileParseError(str(config_path), "配置文件不存在")

        try:
            with open(config_path, "r", encoding="utf-8") as f:
                config_data = yaml.safe_load(f)
            return config_data
        except Exception as e:
            raise FileParseError(str(config_path), str(e))

    def parse_frequency_words(self, words_file: str = None) -> List[Dict]:
        """
        解析关键词配置文件(带 mtime 缓存)

        仅当 frequency_words.txt 被修改时才重新解析,避免循环内重复 IO。

        复用 trendradar.core.frequency 的解析逻辑,支持:
        - # 开头的注释行
        - 空行分隔词组
        - [组别名] 作为词组第一行,给整组指定别名

View on GitHub (pinned to 8ee26026ba)

Solutions

  1. Read the wrapped JSONDecodeError position from the message and inspect content around that offset — 'Expecting value' at line 1 col 1 usually means BOM/HTML, mid-string errors mean truncation.
  2. Strip a leading BOM before parsing: content = content.lstrip('\ufeff').
  3. Re-fetch the feed and check Content-Length vs len(content) to detect truncation; retry on mismatch.
  4. Verify the URL actually serves a JSON Feed (version_url / top-level 'version' key) and that no HTML error page is being passed in.

Example fix

# before
data = json.loads(content)  # raises on BOM/truncated body

# after (defensive caller)
content = content.lstrip('\ufeff')
try:
    items = parser.parse(content, feed_url)
except ValueError as e:
    if len(content) < 200:
        logger.warning("feed %s returned suspiciously short body", feed_url)
    raise
Defensive patterns

Strategy: try-catch

Validate before calling

def json_feed_probe(content: str) -> bool:
    """Verify JSON-decodability before handing to the parser."""
    try:
        json.loads(content.lstrip("\ufeff"))
        return True
    except json.JSONDecodeError:
        return False

Try / catch

try:
    items = parser.parse(content, feed_url)
except ValueError as e:
    if "JSON Feed" in str(e):
        logger.warning("feed %s sent malformed JSON; refetching once", feed_url)
        content = refetch(feed_url)
        items = parser.parse(content, feed_url)  # single retry, not a loop

Prevention

When it happens

Trigger: Calling parse() with content that _is_json_feed classifies as JSON but is malformed: truncated response body (cut mid-download), BOM prefixed to the body (\ufeff{...}) which json.loads rejects, single quotes / trailing commas from a hand-edited file, or content-type sniffing wrong on an HTML fragment that happens to start with '{'.

Common situations: Fetching a JSON feed over an unstable proxy that truncates the body; a server sending UTF-8-BOM; copying feed samples through tools that smart-quote characters; upstream switching from XML to JSON or vice versa so the sniffer picks the wrong branch on partially migrated endpoints.

Related errors


AI-assisted analysis of sansan0/TrendRadar@8ee26026ba (2026-08-15). Data as JSON: /api/errors/dc34d398fa89299e. Report an issue: GitHub.