{"record":{"id":"8430eed166b0f799","repo":"BerriAI/litellm","slug":"rss-feed-missing-channel-element","errorCode":null,"errorMessage":"RSS feed missing <channel> element","messagePattern":"RSS feed missing <channel> element","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"warning","filePath":"litellm/litellm_core_utils/get_blog_posts.py","lineNumber":78,"sourceCode":"        Fetch RSS XML from a remote URL.\n\n        Returns the raw XML text. Raises on network errors.\n        \"\"\"\n        response: Final = httpx.get(url, timeout=timeout)\n        response.raise_for_status()\n        return response.text\n\n    @staticmethod\n    def parse_rss_to_posts(xml_text: str, max_posts: int = 1) -> list[dict[str, str]]:\n        \"\"\"\n        Parse RSS XML and return a list of blog post dicts.\n\n        Extracts title, description, date (YYYY-MM-DD), and url from each <item>.\n        \"\"\"\n        root: Final = ET.fromstring(xml_text)\n        channel: Final = root.find(\"channel\")\n        if channel is None:\n            raise ValueError(\"RSS feed missing <channel> element\")\n\n        posts: Final[list[dict[str, str]]] = []\n        for item in channel.findall(\"item\"):\n            if len(posts) >= max_posts:\n                break\n\n            title_el = item.find(\"title\")\n            link_el = item.find(\"link\")\n            desc_el = item.find(\"description\")\n            pub_date_el = item.find(\"pubDate\")\n\n            if title_el is None or link_el is None:\n                continue\n\n            # Parse RFC 2822 date to YYYY-MM-DD\n            date_str = \"\"\n            if pub_date_el is not None and pub_date_el.text:\n                try:","sourceCodeStart":60,"sourceCodeEnd":96,"githubUrl":"https://github.com/BerriAI/litellm/blob/6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d/litellm/litellm_core_utils/get_blog_posts.py#L60-L96","documentation":"Raised by get_blog_posts.py when parsing the LiteLLM blog RSS feed: the XML parsed successfully but has no <channel> element, so no <item> posts can be extracted. This is an internal utility (used for the 'what's new' blog lookup), not part of the completion API.","triggerScenarios":"Calling get_blog_posts / parse_rss_to_posts when the fetched URL returns XML that is valid but not RSS 2.0 — e.g. an HTML page, an Atom feed, an error page wrapped in XML, or a feed format change.","commonSituations":"The blog endpoint moved or changed format, a proxy/CDN returning an XML error document, or tests feeding fixture XML without a channel element.","solutions":["Verify the feed URL manually with curl and inspect the root element.","If the upstream feed changed format, update the parser (support Atom <feed> or the new location) or pin to the last known-good URL.","For tests, make the fixture include <rss><channel><item>...</item></channel></rss>."],"exampleFix":"# before\nposts = parse_rss_to_posts('<rss></rss>')\n\n# after\nposts = parse_rss_to_posts('<rss><channel><item><title>t</title><link>l</link></item></channel></rss>')","handlingStrategy":"validation","validationCode":"import xml.etree.ElementTree as ET\n\ndef has_channel(xml_text: str) -> bool:\n    try:\n        return ET.fromstring(xml_text).find('channel') is not None\n    except ET.ParseError:\n        return False","typeGuard":null,"tryCatchPattern":"try {\n  parseRssToPosts(xml);\n} catch (e) {\n  if (/missing <channel>/.test(e.message)) { /* skip update, use cached posts */ }\n}","preventionTips":["Validate feed shape (root rss + channel) before parsing.","Cache last-good posts as a fallback display source.","Monitor the feed URL for format changes."],"tags":["rss","parsing","internal-utility","feed-format"],"backgroundTag":null,"analyzedSha":"6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d","analyzedAt":"2026-08-15T07:12:03.035Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}