From 13323620053396c3702c49c499b4c1cf9c017306 Mon Sep 17 00:00:00 2001 From: Julien Herr Date: Mon, 25 May 2026 18:38:38 +0200 Subject: [PATCH] fix(feeds): self link uses configured domain, not request host The RSS/Atom/JSON self link was derived from the request origin, leaking the workers.dev host when reached directly instead of via the custom domain. Use the configured-domain URL builders so self matches alternate. Co-Authored-By: Claude Opus 4.7 --- src/routes/atom.test.ts | 6 ++++-- src/routes/atom.ts | 2 +- src/routes/json.test.ts | 4 +++- src/routes/json.ts | 4 ++-- src/routes/rss.test.ts | 4 +++- src/routes/rss.ts | 2 +- 6 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/routes/atom.test.ts b/src/routes/atom.test.ts index b50299b..0bc1ffc 100644 --- a/src/routes/atom.test.ts +++ b/src/routes/atom.test.ts @@ -117,10 +117,12 @@ describe("Atom Feed Route", () => { expect(body).toContain("Atom Test Feed"); }); - it("self-link points to atom URL", async () => { + it("self-link uses the configured domain, not the request host", async () => { const res = await testApp.request(`/${FEED_ID}`, {}, mockEnv); const body = await res.text(); - expect(body).toContain(`/atom/${FEED_ID}`); + expect(body).toContain( + `rel="self" href="https://${mockEnv.DOMAIN}/atom/${FEED_ID}"`, + ); }); it("Link header advertises hub and self for WebSub discovery", async () => { diff --git a/src/routes/atom.ts b/src/routes/atom.ts index a4c7a72..6e350b9 100644 --- a/src/routes/atom.ts +++ b/src/routes/atom.ts @@ -38,7 +38,7 @@ export async function handle(c: Context<{ Bindings: Env }>): Promise { } const base = baseUrl(c.env); - const selfUrl = new URL(c.req.url).origin + `/atom/${feedId}`; + const selfUrl = feedAtomUrl(feedId, c.env); const atomXml = generateAtomFeed( feedData.feedConfig, feedData.emails, diff --git a/src/routes/json.test.ts b/src/routes/json.test.ts index 22f667f..1d1e0b0 100644 --- a/src/routes/json.test.ts +++ b/src/routes/json.test.ts @@ -52,7 +52,9 @@ describe("JSON Feed Route", () => { const res = await testApp.request("/empty-feed", {}, mockEnv); const link = res.headers.get("Link") ?? ""; expect(link).toContain(`rel="hub"`); - expect(link).toContain(`rel="self"`); + expect(link).toContain( + `; rel="self"`, + ); }); it("body parses as JSON with jsonfeed version 1.1", async () => { diff --git a/src/routes/json.ts b/src/routes/json.ts index 0a74af2..b47c4e6 100644 --- a/src/routes/json.ts +++ b/src/routes/json.ts @@ -2,7 +2,7 @@ import { Context } from "hono"; import { Env } from "../types"; import { generateJsonFeed } from "../infrastructure/feed-generator"; import { fetchFeedData } from "../application/feed-fetcher"; -import { baseUrl } from "../infrastructure/urls"; +import { baseUrl, feedJsonUrl } from "../infrastructure/urls"; import { isExpired } from "../domain/feed"; import { FeedId } from "../domain/value-objects/feed-id"; @@ -22,7 +22,7 @@ export async function handle(c: Context<{ Bindings: Env }>): Promise { } const base = baseUrl(c.env); - const selfUrl = new URL(c.req.url).origin + `/json/${feedId}`; + const selfUrl = feedJsonUrl(feedId, c.env); const jsonFeed = generateJsonFeed( feedData.feedConfig, feedData.emails, diff --git a/src/routes/rss.test.ts b/src/routes/rss.test.ts index c997489..91d3f81 100644 --- a/src/routes/rss.test.ts +++ b/src/routes/rss.test.ts @@ -50,7 +50,9 @@ describe("RSS Feed Route", () => { const res = await testApp.request("/empty-feed", {}, mockEnv); const link = res.headers.get("Link") ?? ""; expect(link).toContain(`rel="hub"`); - expect(link).toContain(`rel="self"`); + expect(link).toContain( + `; rel="self"`, + ); }); }); diff --git a/src/routes/rss.ts b/src/routes/rss.ts index 4ebd1d1..316dbb3 100644 --- a/src/routes/rss.ts +++ b/src/routes/rss.ts @@ -38,7 +38,7 @@ export async function handle(c: Context<{ Bindings: Env }>): Promise { } const base = baseUrl(c.env); - const selfUrl = new URL(c.req.url).origin + `/rss/${feedId}`; + const selfUrl = feedRssUrl(feedId, c.env); const rssXml = generateRssFeed( feedData.feedConfig, feedData.emails,