Validating and troubleshooting feeds

A feed that a browser renders can still be rejected by readers. Validation in the build is the only way to know before subscribers do.

The checks worth automating

# 1. Well-formedness: catches unescaped ampersands and stray markup
xmllint --noout feed.xml

# 2. Structural validation against a schema
xmllint --noout --schema rss-2.0.xsd feed.xml

# 3. Feed-specific rules the W3C validator enforces (dates, guids, absolute URLs)
#    Note that the hosted validator expects a publicly reachable feed URL.

# 4. Practical assertions the validator cannot know about
python - <<'PY'
import sys, feedparser
from datetime import datetime, timezone

f = feedparser.parse("feed.xml")
assert f.version, "not a recognised feed format"
assert f.feed.get("title"), "channel is missing a title"
assert f.feed.get("link"), "channel is missing a link"

newest = None
for e in f.entries:
    assert e.get("link", "").startswith(("http://", "https://")), "relative link"
    assert e.get("id") or e.get("guid"), "item without an identifier"
    if not e.get("published_parsed"):
        print("warning: item without a date:", e.get("title"))
    else:
        d = datetime(*e.published_parsed[:6], tzinfo=timezone.utc)
        assert d <= datetime.now(timezone.utc), "date in the future: " + str(d)
        newest = max(newest or d, d)

assert newest, "feed has no dated items"
print("feed looks publishable; newest item", newest.isoformat())
PY
SymptomCauseFix
Reader shows only the first itemAn unescaped & in later contentEscape every ampersand in text nodes
Titles show as éUTF-8 bytes served as Latin-1Declare the charset in the XML declaration and the HTTP header
Every item is unreadguid changedRestore the old guid values; never derive them from mutable data
Dates display as todayInvalid RFC 822 dateUse +0100 style offsets, not +01:00
Feed works locally, not on the serverCompression or a proxy rewriting the bodyCompare the raw bytes from both environments
Images do not loadRelative URLsMake every URL absolute, including inside content
Reader says the feed is invalidA prefix used but not declaredDeclare every namespace on the rss element
# Validate in the build, so a broken feed never reaches subscribers
import subprocess, sys

def build_and_check(path: str) -> None:
    xml = render_feed()
    open(path, "w", encoding="utf-8").write(xml)
    r = subprocess.run(["xmllint", "--noout", path], capture_output=True, text=True)
    if r.returncode != 0:
        print(r.stderr, file=sys.stderr)
        raise SystemExit("feed is not well-formed XML")

Dates and identifiers, the two chronic problems

from datetime import datetime, timezone

# RSS 2.0 wants RFC 822 with a numeric offset and an English day and month name
def rfc822(dt: datetime) -> str:
    if dt.tzinfo is None:
        raise ValueError("a datetime without a timezone is a bug, not a default")
    return dt.astimezone(timezone.utc).strftime("%a, %d %b %Y %H:%M:%S +0000")

# Atom wants RFC 3339 and requires a timezone
def rfc3339(dt: datetime) -> str:
    if dt.tzinfo is None:
        raise ValueError("a datetime without a timezone is a bug, not a default")
    return dt.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")

# A stable guid: derived from data that never changes, never from the title
def guid(release_id: str) -> str:
    return "release-" + release_id
  • Always use a timezone-aware datetime. A naive datetime serialised as a local time shifts by an hour twice a year and by a day across regions.
  • In RSS 2.0, use a numeric offset such as +0100. The colon form is ISO 8601 and is rejected by strict parsers.
  • Do not use the connection between the guid and the URL unless the URL is genuinely permanent, which requires isPermaLink="true" to be honest.
  • If you must change a guid, accept that every existing subscriber will see the affected items as new. Warn them in a post rather than doing it silently.
⚠️
The lastBuildDate element is not what tells a reader there is new content — readers compare item identifiers and dates. Setting it to the current time on every request signals an update that does not exist, and some readers respond by polling more often.

FAQ

Why does my feed validate but display badly?
Validation checks structure, not rendering. Absolute URLs, a usable summary and a working image are quality issues a validator will not catch, which is why the practical assertions matter.
How often should I test the live feed?
Check well-formedness on every deploy, and run a scheduled daily check against the published URL so an infrastructure change that breaks compression or content type is caught without a subscriber complaint.

Generating a valid feed Podcast feeds and the Apple and Podcasting 2.0 tags

Last refreshed 2026-09-18.