Validador de feed RSS
Todas as regras do RSS 2.0, cada uma com sua referência.
Tudo roda nesta aba. Nada do que você colar é enviado, registrado ou transmitido para lugar nenhum. Abra o painel de rede e confira.
Cole seu feed acima, ou digite o endereço dele e clique em Buscar, e ele é verificado contra a especificação RSS 2.0. Cada achado nomeia o elemento, cita o valor e referencia a regra. Essa última parte importa: cerca de um terço das coisas pelas quais um feed leva bronca são convenções das quais os leitores dependem, e não violações da especificação, e aqui elas são rotuladas como tal.
Você provavelmente está aqui porque algo lá adiante rejeitou o feed. Um leitor mostra o canal mas nenhum episódio. Um serviço de RSS para e-mail recusa. Um assinante diz que todos os posts antigos reapareceram como novos. Cada caso tem uma lista curta de causas prováveis, e todas são verificadas.
Esta página existe porque a ferramenta canônica está se desfazendo em público. Verificado em setembro de 2026, o feedvalidator.org servia um certificado TLS expirado, então os navegadores se recusam a abri-lo, e o espelho dele no SourceForge tem copyright de 2002 a 2004 e ainda destaca o Atom 0.3. O validador do W3C funciona, mas é do lado do servidor: o seu feed vai para a máquina deles.
O que o RSS 2.0 realmente exige
Menos do que a maioria espera. A raiz é <rss> com version="2.0", contendo exatamente um <channel>. O channel precisa de title, link e description, e o link precisa ser uma URL absoluta. Os itens são mais soltos: pelo menos um entre title e description, e mais nada é obrigatório.
O que a especificação não permite é repetição. Dois elementos <title> num channel, ou dois <link> num item, são erros, e o segundo é reportado com a posição dele. Uma verificação estrutural pega um engano fácil: elementos <item> pendurados direto em <rss> em vez de dentro de <channel>. Eles fazem parsing, e nenhum leitor jamais vai mostrá-los.
- Channel: title, link e description obrigatórios.
- Item: pelo menos um entre title e description.
- Elementos únicos não devem se repetir num channel nem num item.
- Os elementos centrais do RSS ficam sem namespace, então <atom:link> não é um <link> de RSS.
Datas são RFC 822, não ISO 8601
O erro que mais parece certo em RSS. pubDate e lastBuildDate precisam ser RFC 822: Wed, 02 Oct 2024 13:00:00 GMT. O que a maioria dos frameworks emite por padrão é ISO 8601, 2024-10-02T13:00:00Z, que está certo para Atom e errado aqui. Um leitor que não consegue analisar um pubDate esconde o item ou o ordena no lugar errado.
Quatro coisas são verificadas. Se a data faz parsing. Se o dia da semana indicado bate com a data, já que a divergência é a digital de um formatador escrito à mão. Se o ano tem dois dígitos, o que o RSS permite e os agregadores leem errado. E se há fuso horário, já que a RFC 822 permite omitir e os leitores então chutam de formas diferentes.
Toda linguagem tem uma resposta de uma chamada só, usada em cada exemplo abaixo: toUTCString, format_datetime, RFC_1123_DATE_TIME, o especificador "r", DATE_RSS.
guid, e por que os assinantes veem seus posts antigos duas vezes
Um guid é a identidade de um item. Os leitores o usam para decidir se algo é novo ou já foi mostrado, então ele precisa ser estável e único. guids duplicados são erro: o leitor trata os dois itens como um e exibe apenas um deles.
A armadilha é o isPermaLink, cujo padrão é true, então um <guid>abc-123</guid> puro afirma que abc-123 é uma URL resolvível. Um identificador opaco precisa de isPermaLink="false" escrito explicitamente, e um guid que não é URL sem isso é erro. Um item sem guid é aviso: os leitores caem para casar por link ou por title.
A versão catastrófica é um plugin que regera os guids a cada build, normalmente a partir de algo volátil como o permalink. Todo assinante recebe o catálogo inteiro de uma vez.
"E comercial", enclosures, e as verificações que não dá para fazer
A maneira mais comum de um feed quebrar é um & solto dentro de uma URL, como em p?id=12&sort=asc. Em XML um "e comercial" inicia uma referência de entidade, então o parsing falha e os leitores rejeitam o documento inteiro em vez daquele item. Você recebe o erro de sintaxe com a posição, e uma nota de que nenhuma regra de RSS pôde rodar.
Um <enclosure> precisa de url, length e type. length é o tamanho em bytes como número inteiro, e 0 é aviso porque significa que o gerador não conseguiu dar stat no arquivo. Dois enclosures num item é aviso do Best Practices Profile, não da especificação.
O channel é checado quanto a um <atom:link rel="self">, que nem sequer é um elemento de RSS mas no qual os agregadores se apoiam. Três verificações aparecem como não executadas, já que um navegador não consegue fazê-las: se rel="self" bate com onde o feed é servido, e se as URLs e os length dos enclosures estão certos.
Verificando um feed em código
Vale rodar no seu pipeline de publicação, porque os modos de falha acima são silenciosos: o feed continua construindo e sendo servido, e você descobre por um assinante. Cada exemplo verifica a regra do item, o formato da data e o 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";Todo exemplo desliga a resolução de entidades externas, porque um feed é um documento que você busca de outro lugar e não escreveu. Em Java e PHP o comportamento inseguro é o padrão.
Perguntas frequentes
Meu feed funciona num leitor e noutro não. O que está errado?
Normalmente uma data, um guid ou a codificação. Os leitores diferem enormemente no que perdoam, então um feed que o Feedly renderiza feliz pode ser rejeitado por um cliente mais rígido.
Vá por partes. Verifique se o documento é sequer XML bem formado, já que um "e comercial" solto quebra tudo dali para a frente. Depois verifique o pubDate de cada item, porque uma data que não faz parsing some com itens ou os ordena de forma estranha. Depois verifique a unicidade dos guids. Se o feed está limpo aqui e um leitor ainda se comporta mal, a causa é o transporte: Content-Type errado, cadeia de redirecionamentos, ou cache.
O feedvalidator.org ainda é a ferramenta que eu deveria usar?
Foi a resposta canônica por vinte anos, mas em setembro de 2026 ele serve um certificado TLS expirado, que os navegadores bloqueiam. O espelho dele no SourceForge tem copyright de 2002 a 2004 e ainda anuncia o Atom 0.3, que a RFC 4287 substituiu em 2005.
O Feed Validation Service do W3C é mantido e o catálogo de erros dele é minucioso. Os limites são idade e arquitetura: mensagens secas, e o seu feed enviado ao servidor deles. Esta página reproduz as verificações daquele catálogo que correspondem a quebras reais e as roda localmente.
Que formato de data o RSS precisa?
RFC 822: Wed, 02 Oct 2024 13:00:00 GMT. Um deslocamento numérico como +0000 é igualmente válido no lugar do nome do fuso. O dia da semana é opcional, mas se você incluir, precisa ser o certo.
ISO 8601, ou seja 2024-10-02T13:00:00Z, não é válido em RSS. É válido em Atom, e é por isso que tantos geradores o emitem: alguém copiou um auxiliar de datas de um template de Atom. Evite anos de dois dígitos e sempre inclua um fuso horário.
Por que todos os meus posts antigos reapareceram como novos para os assinantes?
Algo mudou o guid de todos os itens. Um leitor guarda a lista de guids que já mostrou, e tudo que não está nela é novo. Mude todos de uma vez e cada assinante recebe o arquivo de novo, o que num podcast significa cada episódio baixando em cada aparelho.
A causa é um guid derivado de algo impermanente: um construído a partir do permalink quebra quando você migra para https, um construído a partir do título quebra quando você corrige um erro de digitação. Use uma chave de banco, um UUID ou uma tag URI, com isPermaLink="false".
Meu feed é enviado para um servidor?
Não. O analisador e todas as regras rodam nesta aba como JavaScript. Não há back-end para onde mandar nada nem analytics com acesso ao editor. Abra o painel de rede enquanto valida e veja ele continuar vazio.
Parece pouco importante para um formato tão público quanto o RSS, até você considerar que feeds as pessoas colam num validador: programas que não foram lançados, feeds privados de podcast cujas URLs de enclosure carregam um token por assinante, feeds de homologação cheios de rascunhos.
Isto valida feeds de podcast e tags do iTunes?
Valida a camada RSS 2.0, que é onde começa boa parte das rejeições de podcast: um pubDate malformado, um guid duplicado, um enclosure sem length ou type. Essas regras importam especialmente aqui, já que o enclosure é o episódio.
O que não é verificado é o namespace itunes: itunes:image e as regras de dimensão dele, itunes:category, itunes:explicit, itunes:duration. Eles ficam intocados a não ser que o prefixo esteja sem declaração. Uma regra da Apple vale repetir: cada episódio precisa de um identificador globalmente único que nunca mude, que é a verificação de guid acima.