AJAX RSS Reader

Proxying RSS through PHP, caching, and working around cross-origin limits.

Intermediate
Key PointsBrowsers cannot fetch cross-origin RSS directly — proxy and cache it in PHP.

Why proxy

fetch("https://other-site.com/rss") is blocked by the same-origin policy unless the server sends Access-Control-Allow-Origin. RSS sites almost never do.

So your own server fetches it and the browser calls a same-origin PHP endpoint. Same-origin rules do not apply between servers.

The proxy endpoint

PHP
<?php
// /api/rss.php?feed=runoob
header("Content-Type: application/json; charset=utf-8");

$feeds = [
    "runoob" => "https://www.runoob.com/feed",
    "php"    => "https://www.php.net/feed.atom",
];

$key = $_GET["feed"] ?? "";

// 白名单:绝不能让用户指定任意 URL,否则就是 SSRF
if (!isset($feeds[$key])) {
    http_response_code(400);
    echo json_encode(["ok" => false, "msg" => "未知订阅源"]);
    exit;
}

$cacheFile = sys_get_temp_dir() . "/rss_" . $key . ".json";
$ttl = 600;

if (is_file($cacheFile) && time() - filemtime($cacheFile) < $ttl) {
    echo file_get_contents($cacheFile);
    exit;
}

$ctx = stream_context_create([
    "http" => [
        "timeout" => 5,
        "header"  => "User-Agent: MyReader/1.0\r\n",
    ],
]);

$raw = @file_get_contents($feeds[$key], false, $ctx);

if ($raw === false) {
    http_response_code(502);
    echo json_encode(["ok" => false, "msg" => "抓取失败"]);
    exit;
}

$xml = @simplexml_load_string($raw);
if ($xml === false) {
    http_response_code(502);
    echo json_encode(["ok" => false, "msg" => "不是合法 XML"]);
    exit;
}

$items = [];
foreach ($xml->channel->item ?? [] as $it) {
    $items[] = [
        "title" => (string)$it->title,
        "link"  => (string)$it->link,
        "date"  => (string)$it->pubDate,
    ];
}

$out = json_encode(["ok" => true, "items" => $items], JSON_UNESCAPED_UNICODE);
file_put_contents($cacheFile, $out);

echo $out;

The response shape:

Output
{"ok":true,"items":[{"title":"PHP 8.3 发布","link":"https://www.php.net/archive/2023.php","date":"Wed, 15 Nov 2023 06:13:20 +0000"}]}
The $feeds allowlist is mandatory. Allowing arbitrary URLs enables SSRF — your server reaching cloud metadata at 169.254.169.254 or internal Redis. One of the most dangerous cloud vulnerabilities.

Rendering

JavaScript
async function loadFeed(key) {
  const res  = await fetch(`/api/rss.php?feed=${encodeURIComponent(key)}`);
  const data = await res.json();

  const box = document.querySelector("#feed");

  if (!data.ok) {
    box.innerHTML = `<p class="err">${escapeHtml(data.msg)}</p>`;
    return;
  }

  box.innerHTML = data.items.map(it => `
    <article>
      <h3><a href="${escapeAttr(it.link)}" target="_blank" rel="noopener">${escapeHtml(it.title)}</a></h3>
      <time>${escapeHtml(it.date)}</time>
    </article>
  `).join("");
}
rel="noopener" is required, or the opened page can navigate yours via window.opener — reverse tabnabbing.

Caching

ApproachProsCons
文件缓存零依赖,实现简单多机不共享
Redis多机共享,TTL 精确需额外服务
数据库可审计比文件还重
不缓存实时慢,且容易被封 IP

RSS updates hourly at most, so a 10-minute cache suffices. Without caching every page load hits the source and gets rate-limited fast.

A safer approach

  • Pre-fetch on a cron —— pull every 10 minutes into the database; the API only reads
  • Check Content-Type —— accept only RSS/Atom MIME types
  • Cap the response size —— read at most N KB
  • Block redirects to private IPs —— handle 3xx and validate the target
file_get_contents follows redirects, so a public URL can 302 to http://127.0.0.1/admin and bypass the allowlist. Disable redirects or re-validate each hop.

Friends & Partners