TT-RSS里添加自建的RSSHub源,填好地址点下保存,结果弹了个"无法解析feed";我复制同一个链接到浏览器打开,明明是干干净净的XML,甚至还能看到文章列表。这个场景我遇到过很多次,也在社区里看过不少人在问"究竟是为什么"。TT-RSS是一套自托管的RSS阅读器,RSSHub是把不提供RSS的网页批量生成RSS源的开源工具,很多人会把它们搭在同一台服务器上使用——听起来很顺,但恰恰在"导入源"这一步最容易翻车。
网上关于这个问题的讨论大多各说各话,有人说TT-RSS有bug,有人说RSSHub不够标准。我前前后后折腾过好几轮,结论其实很统一:这两边通常都没坏,只是它们在"一个合法feed应该长什么样"这件事上的默认值不一样。TT-RSS是严格的抓取端,RSSHub自建源是放飞自我的生成端,中间隔着端口、路径、格式、鉴权、证书好几层关卡。这篇文章把完整的排查过程、我踩过的坑、最后稳定运行的配置方案都整理出来,给遇到同样问题的人一条能直接走通的路。
1. 现象还原:TT-RSS导入RSSHub源时到底在报什么错
1.1 三种典型的失败表现
在TT-RSS的订阅管理里添加RSSHub源,常见的失败表现我总结成三类,对应的排查方向完全不同。
第一类是解析失败。点测试之后TT-RSS直接提示"无法解析feed"或者"格式错误",这种最迷惑人,因为浏览器打开明明正常。第二类是网络层错误,比如404、连接超时、SSL证书错误。这类虽然表面是网络问题,但根因往往也不在网络上——多半是URL带上了不该带的尾巴、端口被防火墙拦了、或者反向代理把路径吞了。第三类最隐蔽:添加时显示成功,但后续抓取一直失败,或者抓回来是空数据,一个条目都不显示。
我见过很多人一上来就去改RSSHub配置,结果越改越乱。正确做法是先判断自己属于哪一类:解析失败主要看返回内容的格式;网络层错误主要看URL、端口、证书;"添加成功但没更新"要看TT-RSS的抓取日志和RSSHub端的访问日志。分类对了,后面才不至于南辕北辙。
1.2 一个容易被忽视的细节:浏览器能打开不等于阅读器能导入
浏览器打开RSSHub自建源,和TT-RSS抓取这个源,行为差异其实非常大。
浏览器会跟随重定向,会自动处理各种Content-Type,甚至在响应是JSON时直接给你渲染成一个可读列表;TT-RSS则是一个严格的HTTP客户端,它拿到响应后要交给XML解析器去处理,看到text/html或者application/json就直接判定为失败。其次,浏览器通常带着完整的浏览器指纹发请求,TT-RSS的抓取器有自己的UA(User-Agent),部分自建路由或反向代理会对非浏览器UA做拦截。最后还有一个很多人想不到的点:浏览器打开一个带鉴权的URL时,可能已经带上了Cookie或浏览器缓存好的凭据,而TT-RSS那边是赤裸裸的裸奔请求,什么都没带。
这就是"浏览器正常、TT-RSS失败"这个怪现象的来源。所以排查第一步不是怀疑人生,而是先意识到:这是两个完全不同的客户端,不能用同一个标准去比较。
2. 为什么RSSHub自建源和公共源在TT-RSS眼里根本不一样
2.1 自建源比公共源多出来的几个变量
RSSHub官方公共实例走的是标准HTTPS端口、标准域名,URL形态非常固定,TT-RSS闭着眼都能抓。自建源则完全不一样,它就像你自己在路边开的收费站,每一个环节都可能多出变量。
第一个变量是端口。RSSHub默认监听1200端口,这不是标准的HTTP/S端口。如果TT-RSS在远端服务器上,而云主机的安全组、防火墙没有放行1200,抓取就必然失败。第二个变量是路径。如果你用反向代理,RSSHub可能映射在根路径,也可能映射在某个子目录下,路径写错就是404。第三个变量是鉴权。自建实例可以配置访问密钥(ACCESS_KEY),部分路由需要带access_key参数才能访问,而TT-RSS保存URL时对查询串的处理并不总是如你所愿。第四个变量是HTTPS证书。自己签的证书在浏览器里可以点"继续访问",在TT-RSS的抓取器里那就是硬错误。
这些变量叠加起来,自建源在TT-RSS眼里就是一个"不确定的远程服务",跟公共源的体验天差地别。
2.2 TT-RSS抓取端的两个关键行为
TT-RSS的抓取端有两个行为很容易被忽略,但它们恰恰是很多"奇怪问题"的根源。
第一个是对URL的规范化处理。TT-RSS在保存feed时,会对链接做一套规范化,比如补全协议、去掉多余斜杠、对特殊字符重新编码。这本身是好事,但在带查询参数的URL上会出问题:如果参数里含有&或者其他特殊字符,URL被保存后可能被拆分或转义不完整,尤其是多个参数用&连接的时候,某些情况下可能只保留到第一个&。很多人发现"带access_key的源一开始好好的,某天突然401",多半就是这个原因。
第二个是抓取超时和SSL策略。TT-RSS默认抓取超时并不算长,自建RSSHub如果某个路由后面要聚合多个网页、生成比较慢,首次抓取很容易超时。SSL方面,自建源如果用自签名证书,抓取器会直接报TLS错误,页面上却只显示一个笼统的"抓取失败"。这两个行为叠加起来,会让排错的人很崩溃,因为页面提示完全不具备指向性。
2.3 先搞清楚RSSHub自建源能输出哪几种格式
RSSHub并不是只输出一种格式,同一个路由地址通过参数可以切换输出格式。默认情况下多数路由输出RSS 2.0,加上?format=atom输出Atom,加上?format=json输出JSON Feed。TT-RSS对不同格式的兼容度差别很大。
| 输出格式 | URL参数 | TT-RSS识别情况 | 常见的失败表现 |
|---|---|---|---|
| RSS 2.0 | 默认或?format=rss | 兼容性最好 | 基本无问题 |
| Atom | ?format=atom | 可用,老版本可能不识别部分命名空间 | 解析失败或条目为空 |
| JSON Feed | ?format=json | 兼容性最差 | "无法解析feed" |
这里要特别提醒一个坑:RSSHub某些路由的默认输出并不一定是标准RSS。一些第三方分支、插件会改变默认输出格式,有的路由本身设计的就是输出HTML片段或特殊XML。如果你在TT-RSS里填的地址没有显式指定format=rss,实际拿到的可能根本不是TT-RSS能理解的东西。这也是"究竟是为什么"最常见的答案之一。
3. 从日志和返回内容入手:三步定位断点在哪一段
3.1 第一步:用curl模拟TT-RSS的请求
不要一上来就在TT-RSS界面里反复点测试,那样只能看到"失败",看不到"为什么失败"。我强烈建议先在服务器上,用curl模拟一次TT-RSS式的请求,让网络环境、UA、超时都贴近TT-RSS的真实行为:
curl -L -A "Tiny Tiny RSS" --max-time 10 -i "http://127.0.0.1:1200/some-router/xxx"如果TT-RSS在云端,这个命令就在云端那台机器上跑;如果TT-RSS在本机,就在本机跑。关键是让curl的访问路径和TT-RSS完全一致。加上-i是为了看响应头,重点抓两个字段:HTTP状态码和Content-Type。状态码判断路由是否可达,Content-Type判断返回类型——TT-RSS期待的是application/rss+xml或text/xml,如果你看到application/json,问题就基本清楚了。
如果是HTTPS且用了自签名证书,可以临时加-k参数验证连通性:
curl -k -L -A "Tiny Tiny RSS" -i "https://rss.example.com/some-router/xxx"这一步能把"网络层通不通"和"内容层合不合法"彻底分开。网络不通,先去解决端口、域名、防火墙;网络通但Content-Type不对,就是格式问题,往参数方向查。
3.2 第二步:让TT-RSS自己打出抓取日志
curl能证明源本身是否健康,但还要确认TT-RSS这边实际发生了什么。TT-RSS自带调试日志功能,在设置里打开调试输出,抓取记录会写入日志文件。每次添加源后的测试动作,以及后续定时任务每次拉取,都会留下抓取的URL、状态码、耗时和错误信息。
看日志时要有针对性:看到SSL certificate problem就往证书方向查;看到404 Not Found就往路径和反代方向查;看到JSON parse error或XML解析错误就往格式方向查。还有一个更省事的方法:在RSSHub那台服务器上看实时访问日志,如果TT-RSS的抓取请求根本没到达RSSHub,说明问题出在TT-RSS的URL规范化或网络中间层;如果请求到了但RSSHub返回了非预期内容,那就直接在源端修。
这一步其实能快速排除"有人拦路"的情况。我记得有一次排查到最后,发现是TT-RSS所在的服务器把所有非443端口的出站请求都拦了,curl一测就露馅了。
3.3 第三步:根据响应特征判断断点位置
把curl结果和TT-RSS日志放在一起看,断点非常清晰。我整理了一张对照表,排查时可以对号入座:
| 现象 | 断点位置 | 大概率原因 |
|---|---|---|
| curl正常、TT-RSS解析失败 | 内容层 | 格式不匹配、Content-Type不对 |
| curl正常、TT-RSS网络错误 | 网络层 | UA策略、端口限制或代理配置 |
| curl都失败 | 源站层 | 路由错误、鉴权失效、反代路径 |
| curl失败、但浏览器正常 | 客户端层 | 浏览器自动处理了重定向、UA或鉴权 |
这张表是排查的总纲。我遇到的大多数问题都落在第一行:curl拿到的明明是一份200响应,但Content-Type是application/json或text/html,TT-RSS拿到之后自然无法解析。这种问题你再怎么调TT-RSS都没用,得回到URL参数上去改。
4. 高频原因逐个排查:从URL格式到访问鉴权
4.1 端口、localhost与反向代理路径
自建RSSHub最常见的部署形态是Docker跑在1200端口,然后很多人把http://localhost:1200/xxx直接粘进了TT-RSS。这个操作在浏览器里很可能没问题,因为浏览器就跑在本机;但TT-RSS如果不在同一台机器上,它请求的localhost指向的是它自己,必然扑空。
正确做法要分两种情况。如果RSSHub只监听本机,而TT-RSS在另一台机器,必须让RSSHub监听0.0.0.0,并把防火墙、安全组的1200端口放行;更推荐用反向代理把它收敛到80/443端口。反代之后还要注意路径形态:用子域名反代是最省心的,TT-RSS里直接填https://rss.example.com/xxx;如果用子目录反代,比如https://example.com/rsshub/,就必须确认路径是否被正确改写,否则路由仍然404。
我用nginx做了个子目录反代,最常用的配置是这样:
location /rsshub/ { proxy_pass http://127.0.0.1:1200/; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }注意proxy_pass末尾的/,有了它,/rsshub/这个前缀会被去掉,转发到RSSHub根路径;如果漏了那个斜杠,RSSHub收到的是/rsshub/xxx,而它自己没有这个路由,立刻404。这个斜杠问题是反代配置里最高频的失误,没有之一。
4.2 带鉴权参数的URL在TT-RSS里被"搞坏"
RSSHub配置了访问密钥后,某些路由的URL会变成https://rss.example.com/xxx?access_key=abc123。这种URL在TT-RSS里有不少坑。
第一个坑是&符号。如果URL里还有别的参数,比如?format=atom&access_key=abc123,TT-RSS在保存或规范化时,可能把第二个参数截断。表现就是:测试时正常,过一段时间后突然401或403;或者第一次就失败,但浏览器打开同一个地址完全正常。第二个坑是TT-RSS里的URL重写/过滤器功能。如果你之前为了处理某些源,设置了类似.*access_key.*的规则,它会作用在所有订阅源上,可能把鉴权参数悄悄替换掉。
我的建议很直接:不要把鉴权完全托付给TT-RSS的查询串保存能力。能放在路径里的,就放在路径里;能通过自定义Header传递的,就通过Header传递。RSSHub部分版本支持把访问密钥放在路径形式里,如果条件允许,优先用那种形态。
4.3 HTTPS证书与TLS版本导致的抓取失败
自建RSSHub用自签名证书的话,TT-RSS默认是不会放过的。表现是抓取日志上报"SSL certificate problem",页面上只有一个干巴巴的失败。浏览器会弹警告,你点了"仍然继续",于是浏览器里正常、TT-RSS里不正常的经典案例就诞生了。
解决办法有三个方向。第一,给RSSHub换一张受信任的证书,最省事的方式是用acme脚本自动申请免费证书并配置自动续期,这是最干净的方案,一劳永逸。第二,临时在TT-RSS偏好设置里勾选"允许不安全的SSL证书",但我真心不建议长期这么干,等于把传输链路的加密保护废了一半。第三,检查TT-RSS所在服务器的CA证书库是不是太旧了,老系统CA库更新不及时,会误伤新签发的证书。如果你跑的是很老的PHP版本,对TLS 1.2/1.3的支持也可能不完整,握手阶段就断了。这些问题都不是RSSHub本身的错,而是传输链路的问题。
4.4 最核心的坑:格式参数和路由输出不匹配
如果非要给"究竟是为什么"找一个最大公约数,那一定是格式不匹配。
TT-RSS按标准RSS或Atom来解析,而RSSHub自建源默认输出可能是RSS,也可能是JSON甚至HTML。你从文档里抄来的路由地址,可能默认输出是?format=json,或者你的第三方分支版本改了默认逻辑。还有更隐蔽的情况:反向代理配置里把查询参数剥离了,请求到RSSHub时只剩路径,格式参数根本没传过去。
我的处理思路是:在所有涉及URL的地方,TT-RSS配置、浏览器书签、反代规则,统一显式标出format=rss,确保RSSHub最终收到的是明确的格式指令。验证方法还是curl,把返回内容的前几行打出来,看到<rss>就是RSS 2.0,看到<feed就是Atom,看到{就是JSON。这三个字符决定了完全不同的排错方向。
5. 实测有效的三种配置方案,让TT-RSS长期稳定订阅RSSHub源
5.1 方案A:让RSSHub源按TT-RSS的口味输出标准RSS
对我来说最省事的方案,是在最终URL里显式带format=rss参数:
https://rss.example.com/some-router/article?format=rss如果路由本身还有别的查询参数,就写成?search=keyword&format=rss。粘贴这个完整URL进TT-RSS,基本能解决大部分解析失败问题。但我必须说实话:这个方案的前提是路由本身支持format参数并且实例版本正常。如果你用的是某个魔改分支,或者RSSHub版本太老,format参数可能压根不被处理,那就要回到源端检查默认输出配置。
还有一个更稳的笨办法:在RSSHub的配置里,把默认输出格式固定为RSS。不同版本配置方式略有差异,但思路是一样的——让"默认值"就是TT-RSS最熟悉的东西。这样即使以后有人在URL里漏掉了格式参数,也不会突然挂掉。
5.2 方案B:中间加一层反向代理,把地址收敛成标准HTTPS入口
如果自建RSSHub的内部形态已经比较乱,端口是1200、证书是自签、路径还有子目录,不如在它前面加一层nginx,做成标准化的HTTPS域名入口。这一步同时把4.1和4.3两个问题一起解决掉,属于一劳永逸的做法。
我的常用配置模板:
server { listen 443 ssl; server_name rss.example.com; ssl_certificate /etc/ssl/cert.pem; ssl_certificate_key /etc/ssl/privkey.pem; location / { proxy_pass http://127.0.0.1:1200; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 60s; } }注意这里加了proxy_read_timeout 60s,这是我在实践中加上的——RSSHub某些路由在首次构建时比较慢,如果沿用默认超时,TT-RSS那边会在RSSHub还没响应完就把连接断了。反代完成后,TT-RSS里的源地址统一变成https://rss.example.com/xxx,端口、证书、路径全部收敛到标准形态。以后新增源就不用记内部那些乱七八糟的细节了。
5.3 方案C:从TT-RSS侧调整UA、超时与SSL策略
如果源端不能动,那就调整TT-RSS的行为。
TT-RSS的订阅设置里可以自定义抓取UA,把UA改成常规浏览器UA,可以绕过一些对非浏览器UA的限制。更关键的是开启TT-RSS调试日志,观察每次抓取的耗时和错误,判断是不是超时。如果确实是超时,适当调大超时阈值。SSL方面,如果只是测试,可以在偏好设置里临时允许不安全SSL连接,验证通过后记得改回来。
这里我想泼一盆冷水:方案C属于应急手段,不能当长期习惯。TT-RSS如果跑在公网可访问的服务器上,长期用宽松的UA和SSL策略等于给扫描器开了门。优先考虑方案A和方案B,这才是工程上干净的做法。
5.4 一张自检清单,每次换源失败时照着走
我把排查经验浓缩成这张清单,每条都对应实际见过的坑。遇到新源导入失败,逐项过一遍,通常十分钟内能定位问题:
- [ ] 在TT-RSS所在机器上,curl同一URL是否能正常返回200
- [ ] curl输出是否为合法XML,开头是
<rss或<feed,且Content-Type含xml - [ ] URL是否显式带
format=rss,且参数没被截断、转义 - [ ] 端口是否被TT-RSS侧网络放行,或已通过443反代
- [ ] 证书是否可信,TLS版本是否兼容
- [ ] TT-RSS的UA是否被源站或反代拦截
- [ ] 是否开启过URL重写规则,误伤了该地址
我把这张表存在服务器配置目录里,排查新源失败的时候能省很多时间。
6. 这类问题背后是一类通病:自托管服务之间的"互认"
6.1 以"阅读器客户端"视角测试,别用浏览器视角
TT-RSS导入RSSHub失败,本质上属于"自托管服务A访问自托管服务B时的互认"问题。这个坑在很多组合里都会出现:定时任务去抓自建API、监控系统去探测自建站点、日历应用去订阅自建ICS。它们的共性是:服务A用服务端网络、严格的客户端行为去访问,而不是用渲染引擎。
理解了这一点,你就会养成一个习惯:凡是"浏览器能打开、服务端不行"的问题,先在服务端用curl试一次,把返回的内容当字符串看,而不是当渲染结果看。这个习惯可以帮你避开大量"我觉得"式的无效排错。我见过有人在TT-RSS和RSSHub两端各折腾了几天,结果问题只是UA被反代拦截,curl里加个-A就复现了。
6.2 定时抓取要站在源站的资源角度考虑
TT-RSS是按周期抓取的,RSSHub是动态生成源,如果某条路由要聚合多个网页,每次抓取都会给RSSHub带来不小的计算开销。如果TT-RSS默认的抓取间隔设置得比较短,多个源同时更新时,自建源可能因为忙于构建而返回超时或5xx,TT-RSS又会把这记成一次失败。
我建议把更新频繁的自建源单独设置一个更长的抓取间隔,同时开启RSSHub侧的缓存,减少重复构建。这个优化虽然不是"导入失败"的直接原因,但能避免"今天能导、明天又挂"的反复折腾。自托管服务最重要的品质不是功能多,而是稳定。
6.3 同类源:静态博客feed、开放API feed也能用这套思路
这套排查流程不只适用于RSSHub。静态博客生成器输出的/atom.xml、/feed.xml,API网关转发的feed,甚至内网里的资讯接口,遇到TT-RSS导入失败时都能套同一套流程:先确认源站可达,再确认Content-Type和XML格式,再检查SSL、UA、超时。流程是通用的,只是具体报错文案不一样。
我能给的最后一个建议是:在服务器上保存一组curl测试命令,把每个关键源的验证命令都写在一个脚本里。半分钟就能跑完一轮"TT-RSS视角"的请求,比在界面里反复点测试有用得多。这种方法一开始有点土,但用过的都知道它的价值。
最后说说我个人的排错习惯。每次遇到"TT-RSS无法导入RSSHub自建源",我不会先去翻TT-RSS源码,也不会急着重装RSSHub,而是先问自己一句:如果我是TT-RSS,我去请求这个地址,会看到什么?答案百分之八十藏在curl返回的前几行字符里。按照这个思路,我基本十分钟内能把问题定位到网络层、内容层还是格式层。再分享一个小技巧:在TT-RSS添加源界面粘贴URL之前,先手动在URL末尾加一个不常用的参数,比如&debug=ttrss_test,然后在RSSHub实时日志里看这次请求是否真的到达、带上了哪些参数。这个方法能直接验证TT-RSS到底发出去了什么,比任何猜测都管用。