From e031209d689b98afa76a9e3351214d6a1ac33e26 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 22 Sep 2026 23:32:37 -0700 Subject: [PATCH] caching --- src/widgets/feed/proxy.js | 42 +++++++++++++++++++++------------- src/widgets/feed/proxy.test.js | 35 ++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 16 deletions(-) diff --git a/src/widgets/feed/proxy.js b/src/widgets/feed/proxy.js index ac4cccb9a..7f58460c3 100644 --- a/src/widgets/feed/proxy.js +++ b/src/widgets/feed/proxy.js @@ -1,3 +1,5 @@ +import cache from "memory-cache"; + import { parseFeed } from "./utils"; import getServiceWidget from "utils/config/service-helpers"; @@ -6,6 +8,7 @@ import { sanitizeErrorURL } from "utils/proxy/api-helpers"; import { httpProxy } from "utils/proxy/http"; const logger = createLogger("feedProxyHandler"); +const CACHE_MS = 10 * 60 * 1000; export default async function feedProxyHandler(req, res) { const { group, service, index } = req.query; @@ -22,24 +25,31 @@ export default async function feedProxyHandler(req, res) { return res.status(400).json({ error: "Invalid feed URL" }); } - const [status, , data] = await httpProxy(url, { - headers: { - "User-Agent": `gethomepage/${process.env.NEXT_PUBLIC_VERSION || "dev"}`, - Accept: "application/rss+xml, application/atom+xml, application/xml;q=0.9, text/xml;q=0.9, */*;q=0.8", - }, - }); + const cacheKey = `feed:${url.href}`; + let items = cache.get(cacheKey); - if (status !== 200) { - logger.debug("HTTP %d retrieving feed %s//%s%s", status, url.protocol, url.host, url.pathname); - return res.status(status).json({ error: { message: "HTTP Error", url: sanitizeErrorURL(url) } }); - } + if (!items) { + const [status, , data] = await httpProxy(url, { + headers: { + "User-Agent": `gethomepage/${process.env.NEXT_PUBLIC_VERSION || "dev"}`, + Accept: "application/rss+xml, application/atom+xml, application/xml;q=0.9, text/xml;q=0.9, */*;q=0.8", + }, + }); - let items; - try { - items = parseFeed(Buffer.from(data).toString(), url.href); - } catch (e) { - logger.debug("Error parsing feed %s//%s%s: %s", url.protocol, url.host, url.pathname, e.message); - return res.status(500).json({ error: { message: "Invalid feed", url: sanitizeErrorURL(url) } }); + if (status !== 200) { + logger.debug("HTTP %d retrieving feed %s//%s%s", status, url.protocol, url.host, url.pathname); + return res.status(status).json({ error: { message: "HTTP Error", url: sanitizeErrorURL(url) } }); + } + + try { + items = parseFeed(Buffer.from(data).toString(), url.href); + } catch (e) { + logger.debug("Error parsing feed %s//%s%s: %s", url.protocol, url.host, url.pathname, e.message); + return res.status(500).json({ error: { message: "Invalid feed", url: sanitizeErrorURL(url) } }); + } + + // only successful fetches are cached + cache.put(cacheKey, items, CACHE_MS); } const maxItems = parseInt(widget.maxItems, 10) || 5; diff --git a/src/widgets/feed/proxy.test.js b/src/widgets/feed/proxy.test.js index 79526b98a..f713ea835 100644 --- a/src/widgets/feed/proxy.test.js +++ b/src/widgets/feed/proxy.test.js @@ -1,3 +1,4 @@ +import cache from "memory-cache"; import { beforeEach, describe, expect, it, vi } from "vitest"; import createMockRes from "test-utils/create-mock-res"; @@ -36,6 +37,7 @@ const req = { query: { group: "g", service: "svc", index: "0" } }; describe("widgets/feed/proxy", () => { beforeEach(() => { vi.clearAllMocks(); + cache.clear(); }); it("returns 400 when the url is missing or invalid", async () => { @@ -110,4 +112,37 @@ describe("widgets/feed/proxy", () => { expect(res.statusCode).toBe(500); expect(res.body).toEqual({ error: { message: "Invalid feed", url: "example.com (see logs for details)" } }); }); + + it("shares one cached fetch across widgets with different options", async () => { + getServiceWidget + .mockResolvedValueOnce({ type: "feed", url: "https://example.com/feed.xml", maxItems: 1 }) + .mockResolvedValueOnce({ type: "feed", url: "https://example.com/feed.xml", images: false }); + httpProxy.mockResolvedValue([200, "application/rss+xml", Buffer.from(feed)]); + + const first = createMockRes(); + await feedProxyHandler(req, first); + const second = createMockRes(); + await feedProxyHandler(req, second); + + expect(httpProxy).toHaveBeenCalledTimes(1); + expect(first.body.items).toHaveLength(1); + expect(second.body.items).toHaveLength(5); + expect(second.body.items[0].image).toBeUndefined(); + }); + + it("does not cache failures", async () => { + getServiceWidget.mockResolvedValue({ type: "feed", url: "https://example.com/feed.xml" }); + httpProxy + .mockResolvedValueOnce([404, "text/html", Buffer.from("Not found")]) + .mockResolvedValueOnce([200, "application/rss+xml", Buffer.from(feed)]); + + const failed = createMockRes(); + await feedProxyHandler(req, failed); + const recovered = createMockRes(); + await feedProxyHandler(req, recovered); + + expect(failed.statusCode).toBe(404); + expect(recovered.statusCode).toBe(200); + expect(httpProxy).toHaveBeenCalledTimes(2); + }); });