RSS-Feed-Validator
Jede RSS-2.0-Regel, jede mit ihrer Fundstelle.
Alles läuft in diesem Tab. Nichts, was Sie einfügen, wird hochgeladen, protokolliert oder irgendwohin gesendet. Öffnen Sie Ihr Netzwerkpanel und prüfen Sie es.
Fügen Sie Ihren Feed oben ein oder geben Sie seine Adresse ein und drücken Sie Abrufen: Er wird gegen die RSS-2.0-Spezifikation geprüft. Jeder Befund nennt das Element, zitiert den Wert und verweist auf die Regel. Letzteres zählt: Rund ein Drittel dessen, wofür ein Feed gerügt wird, sind Konventionen, auf die sich Reader verlassen, und keine Spezifikationsverstöße – und genau so werden sie hier gekennzeichnet.
Sie sind vermutlich hier, weil etwas weiter unten den Feed abgelehnt hat. Ein Reader zeigt den Kanal, aber keine Folgen. Ein RSS-zu-E-Mail-Dienst verweigert ihn. Ein Abonnent sagt, alle alten Beiträge seien als neu wieder aufgetaucht. Jeder Fall hat eine kurze Liste wahrscheinlicher Ursachen, und alle werden geprüft.
Diese Seite existiert, weil das kanonische Werkzeug öffentlich zerfällt. Im September 2026 geprüft, lieferte feedvalidator.org ein abgelaufenes TLS-Zertifikat aus, sodass Browser es nicht öffnen, und sein SourceForge-Spiegel trägt Copyright 2002 bis 2004 und wirbt immer noch mit Atom 0.3. Der W3C-Validator funktioniert, ist aber serverseitig: Ihr Feed geht auf deren Maschine.
Was RSS 2.0 tatsächlich verlangt
Weniger, als die meisten erwarten. Die Wurzel ist <rss> mit version="2.0" und enthält genau ein <channel>. Der Kanal braucht title, link und description, und der link muss eine absolute URL sein. Items sind lockerer: mindestens eines von title oder description, sonst ist nichts erforderlich.
Was die Spezifikation nicht erlaubt, ist Wiederholung. Zwei <title>-Elemente in einem Kanal oder zwei <link> in einem Item sind Fehler, und das zweite wird mit seiner Position gemeldet. Eine Strukturprüfung fängt einen leichten Fehler ab: <item>-Elemente, die direkt unter <rss> statt innerhalb von <channel> stehen. Sie parsen sauber, und kein Reader wird sie je zeigen.
- Kanal: title, link und description erforderlich.
- Item: mindestens eines von title oder description.
- Einmalige Elemente dürfen sich in einem Kanal oder Item nicht wiederholen.
- Die RSS-Kernelemente stehen in keinem Namensraum, <atom:link> ist also kein RSS-<link>.
Datumsangaben sind RFC 822, nicht ISO 8601
Der Fehler in RSS, der am gültigsten aussieht. pubDate und lastBuildDate müssen RFC 822 sein: Wed, 02 Oct 2024 13:00:00 GMT. Was die meisten Frameworks standardmäßig ausgeben, ist ISO 8601, 2024-10-02T13:00:00Z – richtig für Atom und falsch hier. Ein Reader, der ein pubDate nicht parsen kann, blendet das Item aus oder sortiert es an die falsche Stelle.
Vier Dinge werden geprüft. Ob sich das Datum überhaupt parsen lässt. Ob der genannte Wochentag zum Datum passt, denn eine Abweichung ist der Fingerabdruck eines von Hand geschriebenen Formatierers. Ob das Jahr zweistellig ist, was RSS erlaubt und Aggregatoren falsch lesen. Und ob eine Zeitzone da ist, denn RFC 822 lässt zu, sie wegzulassen, und Reader raten dann unterschiedlich.
Jede Sprache hat eine Ein-Aufruf-Antwort, in jedem Beispiel unten verwendet: toUTCString, format_datetime, RFC_1123_DATE_TIME, der Spezifizierer „r“, DATE_RSS.
guid, und warum Abonnenten Ihre alten Beiträge doppelt sehen
Ein guid ist die Identität eines Items. Reader entscheiden damit, ob etwas neu oder schon gezeigt ist, er muss also stabil und eindeutig sein. Doppelte guids sind ein Fehler: Ein Reader behandelt die beiden Items als eines und zeigt nur eines davon.
Die Falle ist isPermaLink, dessen Standardwert true ist – ein nacktes <guid>abc-123</guid> behauptet also, abc-123 sei eine auflösbare URL. Ein undurchsichtiger Bezeichner braucht ein ausdrücklich geschriebenes isPermaLink="false", und ein Nicht-URL-guid ohne das ist ein Fehler. Ein Item ohne guid ist eine Warnung: Reader weichen auf einen Abgleich über link oder title aus.
Die katastrophale Variante ist ein Plugin, das guids bei jedem Build neu erzeugt, meist aus etwas Flüchtigem wie dem Permalink. Jeder Abonnent bekommt dann den gesamten Rückkatalog auf einmal.
Und-Zeichen, Enclosures, und die Prüfungen, die nicht laufen können
Am häufigsten bricht ein Feed an einem rohen & in einer URL, wie in p?id=12&sort=asc. In XML beginnt ein Und-Zeichen eine Entity-Referenz, das Parsen scheitert also, und Reader verwerfen das ganze Dokument statt des einen Items. Sie bekommen den Syntaxfehler mit Position und den Hinweis, dass keine RSS-Regel laufen konnte.
Ein <enclosure> braucht url, length und type. length ist die Größe in Byte als ganze Zahl, und 0 ist eine Warnung, weil es bedeutet, dass der Generator die Datei nicht statten konnte. Zwei Enclosures an einem Item sind eine Warnung aus dem Best Practices Profile, nicht aus der Spezifikation.
Der Kanal wird auf ein <atom:link rel="self"> geprüft, das gar kein RSS-Element ist, auf das sich Aggregatoren aber stützen. Drei Prüfungen erscheinen als nicht gelaufen, weil ein Browser sie nicht machen kann: ob rel="self" dazu passt, wo der Feed ausgeliefert wird, und ob die URLs und Längen der Enclosures stimmen.
Einen Feed in Code prüfen
Lohnt sich in Ihrer Veröffentlichungs-Pipeline, denn die obigen Fehlerbilder sind still: Der Feed baut und liefert weiterhin aus, und Sie erfahren es von einem Abonnenten. Jedes Beispiel prüft die Item-Regel, das Datumsformat und den guid.
// Node 18 or later. npm i @xmldom/xmldom
import { DOMParser } from '@xmldom/xmldom';
const xml = await (await fetch('https://example.com/feed.xml')).text();
const doc = new DOMParser().parseFromString(xml, 'text/xml');
const channel = doc.getElementsByTagName('channel').item(0);
for (const required of ['title', 'link', 'description']) {
if (channel.getElementsByTagName(required).length === 0) {
console.error('channel has no <' + required + '>');
}
}
// RFC 822, not ISO 8601. Anything with a "T" separator is the wrong format.
const RFC822 = /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{2} (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}(:\d{2})? (GMT|UT|[A-Z]{3}|[+-]\d{4})$/;
const items = channel.getElementsByTagName('item');
for (let i = 0; i < items.length; i++) {
const item = items.item(i);
const text = (name) => {
const el = item.getElementsByTagName(name).item(0);
return el ? el.textContent.trim() : null;
};
if (!text('title') && !text('description')) {
console.error('item ' + (i + 1) + ' has neither a title nor a description');
}
const pubDate = text('pubDate');
if (pubDate && !RFC822.test(pubDate)) {
console.error('item ' + (i + 1) + ' pubDate is not RFC 822: ' + pubDate);
}
const guid = item.getElementsByTagName('guid').item(0);
if (guid && guid.getAttribute('isPermaLink') !== 'false'
&& !/^https?:\/\//.test(guid.textContent.trim())) {
console.error('item ' + (i + 1) + ' guid defaults to isPermaLink="true" but is not a URL');
}
}
// Producing a correct date is one call:
console.log(new Date().toUTCString()); // Wed, 02 Oct 2024 13:00:00 GMT# pip install lxml requests
import sys
import requests
from email.utils import format_datetime, parsedate_to_datetime
from datetime import datetime, timezone
from lxml import etree
raw = requests.get('https://example.com/feed.xml', timeout=30).content
# resolve_entities=False and no_network=True are the safe form.
parser = etree.XMLParser(resolve_entities=False, no_network=True, load_dtd=False)
root = etree.fromstring(raw, parser)
channel = root.find('channel')
if channel is None:
sys.exit('No <channel> element')
for required in ('title', 'link', 'description'):
if channel.find(required) is None:
print('channel has no <%s>' % required)
for i, item in enumerate(channel.findall('item'), start=1):
if item.find('title') is None and item.find('description') is None:
print('item %d has neither a title nor a description' % i)
pub = item.findtext('pubDate')
if pub:
try:
# parsedate_to_datetime is the RFC 2822 parser, and it rejects
# ISO 8601 outright, which is exactly what you want here.
parsedate_to_datetime(pub)
except (TypeError, ValueError):
print('item %d pubDate is not RFC 822: %s' % (i, pub))
guid = item.find('guid')
if guid is not None and guid.get('isPermaLink', 'true') == 'true':
if not (guid.text or '').startswith(('http://', 'https://')):
print('item %d guid is treated as a permalink but is not a URL' % i)
# Producing a correct date:
print(format_datetime(datetime.now(timezone.utc)))import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.ByteArrayInputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.ZonedDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
byte[] bytes = HttpClient.newHttpClient()
.send(HttpRequest.newBuilder(URI.create("https://example.com/feed.xml")).build(),
HttpResponse.BodyHandlers.ofByteArray())
.body();
DocumentBuilderFactory f = DocumentBuilderFactory.newInstance();
f.setNamespaceAware(true);
f.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
f.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
f.setXIncludeAware(false);
Document doc = f.newDocumentBuilder().parse(new ByteArrayInputStream(bytes));
Element channel = (Element) doc.getElementsByTagName("channel").item(0);
for (String required : new String[] { "title", "link", "description" }) {
if (channel.getElementsByTagName(required).getLength() == 0) {
System.err.println("channel has no <" + required + ">");
}
}
NodeList items = channel.getElementsByTagName("item");
for (int i = 0; i < items.getLength(); i++) {
Element item = (Element) items.item(i);
boolean hasTitle = item.getElementsByTagName("title").getLength() > 0;
boolean hasDesc = item.getElementsByTagName("description").getLength() > 0;
if (!hasTitle && !hasDesc) {
System.err.println("item " + (i + 1) + " has neither a title nor a description");
}
NodeList pub = item.getElementsByTagName("pubDate");
if (pub.getLength() > 0) {
String v = pub.item(0).getTextContent().trim();
try {
// RFC_1123_DATE_TIME is the RFC 822 profile RSS wants, and it
// refuses ISO 8601, so a wrong-format date fails loudly here.
DateTimeFormatter.RFC_1123_DATE_TIME.parse(v);
} catch (DateTimeParseException e) {
System.err.println("item " + (i + 1) + " pubDate is not RFC 822: " + v);
}
}
}
// Producing a correct date:
System.out.println(DateTimeFormatter.RFC_1123_DATE_TIME
.format(ZonedDateTime.now(ZoneOffset.UTC)));using System.Globalization;
using System.Xml;
using System.Xml.Linq;
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit, // external entities are never fetched
XmlResolver = null,
};
var bytes = await new HttpClient().GetByteArrayAsync("https://example.com/feed.xml");
using var stream = new MemoryStream(bytes);
using var reader = XmlReader.Create(stream, settings);
var doc = XDocument.Load(reader);
// RSS core elements are in no namespace, so plain element names are correct here.
var channel = doc.Root?.Element("channel");
if (channel is null) throw new InvalidOperationException("No <channel> element");
foreach (var required in new[] { "title", "link", "description" })
{
if (channel.Element(required) is null)
Console.Error.WriteLine("channel has no <" + required + ">");
}
int n = 0;
foreach (var item in channel.Elements("item"))
{
n++;
if (item.Element("title") is null && item.Element("description") is null)
Console.Error.WriteLine("item " + n + " has neither a title nor a description");
var pub = (string?)item.Element("pubDate");
if (pub is not null)
{
// .NET has no dedicated RFC 822 parser. TryParse handles the usual
// spellings but also accepts ISO 8601, which RSS forbids, so reject
// anything carrying a "T" separator before parsing.
if (pub.Contains('T') ||
!DateTimeOffset.TryParse(pub, CultureInfo.InvariantCulture,
DateTimeStyles.None, out _))
{
Console.Error.WriteLine("item " + n + " pubDate is not RFC 822: " + pub);
}
}
var guid = item.Element("guid");
if (guid is not null && (string?)guid.Attribute("isPermaLink") != "false"
&& !guid.Value.StartsWith("http", StringComparison.Ordinal))
{
Console.Error.WriteLine("item " + n + " guid is a permalink by default but is not a URL");
}
}
// Producing a correct date: "r" is the RFC 1123 pattern RSS accepts.
Console.WriteLine(DateTimeOffset.UtcNow.ToString("r", CultureInfo.InvariantCulture));<?php
// LIBXML_NONET stops libxml fetching anything the document references.
libxml_use_internal_errors(true);
$raw = file_get_contents('https://example.com/feed.xml');
$feed = simplexml_load_string($raw, 'SimpleXMLElement', LIBXML_NONET);
if ($feed === false) {
foreach (libxml_get_errors() as $e) {
fprintf(STDERR, "XML error at line %d: %s", $e->line, trim($e->message) . "\n");
}
exit(1);
}
foreach (['title', 'link', 'description'] as $required) {
if (!isset($feed->channel->{$required})) {
fprintf(STDERR, "channel has no <%s>\n", $required);
}
}
$n = 0;
foreach ($feed->channel->item as $item) {
$n++;
if (!isset($item->title) && !isset($item->description)) {
fprintf(STDERR, "item %d has neither a title nor a description\n", $n);
}
if (isset($item->pubDate)) {
$raw_date = (string) $item->pubDate;
// DATE_RSS expects a numeric offset; the second pattern covers "GMT".
$parsed = DateTimeImmutable::createFromFormat(DATE_RSS, $raw_date)
?: DateTimeImmutable::createFromFormat('D, d M Y H:i:s T', $raw_date);
if ($parsed === false) {
fprintf(STDERR, "item %d pubDate is not RFC 822: %s\n", $n, $raw_date);
}
}
$guid = $item->guid;
if ($guid !== null && (string) ($guid['isPermaLink'] ?? 'true') !== 'false'
&& !preg_match('#^https?://#', (string) $guid)) {
fprintf(STDERR, "item %d guid defaults to isPermaLink=true but is not a URL\n", $n);
}
}
// Producing a correct date: DATE_RSS is RFC 822.
echo (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format(DATE_RSS), "\n";Jedes Beispiel schaltet die Auflösung externer Entities ab, denn ein Feed ist ein Dokument, das Sie anderswo abrufen und nicht selbst geschrieben haben. In Java und PHP ist das unsichere Verhalten der Standard.
Häufige Fragen
Mein Feed funktioniert in einem Reader und im anderen nicht. Was stimmt nicht?
Meist ein Datum, ein guid oder die Kodierung. Reader unterscheiden sich enorm darin, was sie verzeihen, ein Feed, den Feedly munter rendert, kann von einem strengeren Client abgelehnt werden.
Arbeiten Sie es der Reihe nach ab. Prüfen Sie, ob das Dokument überhaupt wohlgeformtes XML ist, denn ein rohes Und-Zeichen bricht alles Nachgelagerte. Dann das pubDate jedes Items, denn ein unparsbares Datum lässt Items verschwinden oder seltsam sortieren. Dann die Eindeutigkeit der guids. Ist der Feed hier sauber und ein Reader spinnt weiterhin, liegt es am Transport: falscher Content-Type, eine Weiterleitungskette oder Caching.
Ist feedvalidator.org noch das Werkzeug meiner Wahl?
Zwanzig Jahre lang war es die kanonische Antwort, aber seit September 2026 liefert es ein abgelaufenes TLS-Zertifikat aus, das Browser blockieren. Sein SourceForge-Spiegel trägt Copyright 2002 bis 2004 und bewirbt weiterhin Atom 0.3, das RFC 4287 im Jahr 2005 ersetzt hat.
Der Feed Validation Service des W3C wird gepflegt, und sein Fehlerkatalog ist gründlich. Seine Grenzen sind Alter und Architektur: knappe Meldungen und Ihr Feed auf deren Server. Diese Seite bildet die Prüfungen aus jenem Katalog nach, die echten Ausfällen entsprechen, und führt sie lokal aus.
Welches Datumsformat braucht RSS?
RFC 822: Wed, 02 Oct 2024 13:00:00 GMT. Ein numerischer Versatz wie +0000 ist statt des Zonennamens genauso gültig. Der Wochentag ist optional, aber wenn Sie ihn angeben, muss er der richtige sein.
ISO 8601, also 2024-10-02T13:00:00Z, ist in RSS nicht gültig. In Atom ist es das, und deshalb geben so viele Generatoren es aus: Jemand hat einen Datums-Helfer aus einem Atom-Template kopiert. Vermeiden Sie zweistellige Jahre und geben Sie immer eine Zeitzone an.
Warum tauchten alle meine alten Beiträge bei den Abonnenten als neu auf?
Etwas hat den guid jedes Items geändert. Ein Reader führt eine Liste der guids, die er gezeigt hat, und alles, was nicht darauf steht, ist neu. Ändern Sie alle auf einmal, bekommt jeder Abonnent das Archiv erneut – bei einem Podcast heißt das jede Folge auf jedes Gerät.
Die Ursache ist ein guid, der aus etwas Unbeständigem abgeleitet wurde: einer aus dem Permalink bricht beim Umzug auf https, einer aus dem Titel bricht, wenn Sie einen Tippfehler korrigieren. Nehmen Sie einen Datenbankschlüssel, eine UUID oder eine Tag-URI, mit isPermaLink="false".
Wird mein Feed an einen Server geschickt?
Nein. Der Parser und jede Regel laufen in diesem Tab als JavaScript. Es gibt kein Backend, an das sich etwas senden ließe, und keine Analytics mit Zugriff auf den Editor. Öffnen Sie beim Validieren Ihr Netzwerkpanel und sehen Sie zu, wie es leer bleibt.
Bei einem so öffentlichen Format wie RSS wirkt das unwichtig – bis man überlegt, welche Feeds Leute in einen Validator einfügen: Shows, die noch nicht gestartet sind, private Podcast-Feeds, deren Enclosure-URLs ein Token je Abonnent tragen, Staging-Feeds voller Entwürfe.
Validiert das auch Podcast-Feeds und iTunes-Tags?
Es validiert die RSS-2.0-Ebene, und dort beginnt ein großer Teil der Podcast-Ablehnungen: ein fehlerhaftes pubDate, ein doppelter guid, ein Enclosure ohne length oder type. Diese Regeln zählen hier besonders, denn das Enclosure ist die Folge.
Was es nicht prüft, ist der itunes-Namensraum: itunes:image und seine Größenregeln, itunes:category, itunes:explicit, itunes:duration. Die bleiben unangetastet, sofern ihr Präfix nicht undeklariert ist. Eine Apple-Regel sei wiederholt: Jede Folge braucht einen global eindeutigen Bezeichner, der sich nie ändert – das ist die guid-Prüfung oben.