From aea9796170fa4af15de00c548e4911562fa0e96e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 7 Nov 2026 01:26:17 +0100 Subject: [PATCH] fix(cycle): raise the calendar feed fetch limits A link subscribed on a phone, a tablet and a computer at a five-minute refresh came close to the 30 fetches an hour a link was allowed, and a household behind one address shared 120. The per-link bucket is now 120 an hour and the per-address one 600. The token is a random secret checked by its HMAC, so guessing links is not what these limits stop. --- src/lib/cycle/feed/__tests__/serve.test.ts | 4 ++-- src/lib/cycle/feed/serve.ts | 16 +++++++++++----- tests/integration/cycle-calendar-feed.test.ts | 2 +- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/lib/cycle/feed/__tests__/serve.test.ts b/src/lib/cycle/feed/__tests__/serve.test.ts index 4aa12ccc8..a822043cc 100644 --- a/src/lib/cycle/feed/__tests__/serve.test.ts +++ b/src/lib/cycle/feed/__tests__/serve.test.ts @@ -145,7 +145,7 @@ describe("serveCycleFeed", () => { expect(limits.surface).toHaveBeenCalledWith( expect.anything(), "cycle-feed", - 120, + 600, 3_600_000, ); }); @@ -156,7 +156,7 @@ describe("serveCycleFeed", () => { expect(res.status).toBe(429); expect(limits.token).toHaveBeenCalledWith( `cycle-feed:${hashToken(TOKEN)}`, - 30, + 120, 3_600_000, ); expect(built.build).not.toHaveBeenCalled(); diff --git a/src/lib/cycle/feed/serve.ts b/src/lib/cycle/feed/serve.ts index 73295d05a..c953d44f5 100644 --- a/src/lib/cycle/feed/serve.ts +++ b/src/lib/cycle/feed/serve.ts @@ -56,12 +56,18 @@ import { parseFeedTokenSegment } from "./token"; /** * Per source address, across every feed. Generous because the big calendar - * providers poll from a pool of addresses that churns; the per-token bucket - * below is the one that bounds a single link. + * providers poll from a pool of addresses that churns, and a household behind + * one address may hold several links on several devices; the per-token bucket + * below is the one that bounds a single link. The token is a random secret + * checked by its HMAC, so guessing links is not what these limits stop. */ -const SURFACE_LIMIT_PER_HOUR = 120; -/** Per link. A provider polling every few minutes still fits. */ -const TOKEN_LIMIT_PER_HOUR = 30; +const SURFACE_LIMIT_PER_HOUR = 600; +/** + * Per link. One link subscribed on a phone, a tablet and a computer, each + * refreshing every five minutes, is 36 fetches an hour; 120 leaves room for + * a fourth device and for retries. + */ +const TOKEN_LIMIT_PER_HOUR = 120; const HOUR_MS = 60 * 60 * 1000; /** The longest User-Agent kept for the status read. */ diff --git a/tests/integration/cycle-calendar-feed.test.ts b/tests/integration/cycle-calendar-feed.test.ts index b615b2755..adc7bcf04 100644 --- a/tests/integration/cycle-calendar-feed.test.ts +++ b/tests/integration/cycle-calendar-feed.test.ts @@ -510,7 +510,7 @@ describe("fetching the feed", () => { it("throttles a well-formed unknown token through the anonymous limit", async () => { let throttled = false; - for (let i = 0; i < 130 && !throttled; i++) { + for (let i = 0; i < 610 && !throttled; i++) { const res = await fetchFeed(`hlf_${"1".repeat(48)}.ics`); if (res.status === 429) throttled = true; else expect(res.status).toBe(404);