fix: forward the visitor address on route-cached renders

Nitro's cached handler renders from a header-less copy of the request, so
under swr route rules useJournalApi, /rss.xml and the sitemap source
forwarded only the Nuxt peer and every cache miss shared one Journal API
rate-limit bucket (deferred in Phase 16). A Nitro plugin records
x-forwarded-for plus the peer in event.context.journalForwardedFor on the
request hook; the three callers forward it. Same key as the host app's
workaround, which keeps working. The acme SSR smoke's x-forwarded-for check
on the now route-cached playground is the test.
This commit is contained in:
2026-10-10 18:40:25 +02:00
parent 4f55511ece
commit 03a90e952d
5 changed files with 42 additions and 10 deletions

View File

@@ -15,9 +15,13 @@ export default defineSitemapEventHandler(async (event) => {
const base = (config.journal as { apiBase: string }).apiBase
const routes = (config.public.journal as { routes: JournalRoutes }).routes
const forwarded = [getRequestHeader(event, 'x-forwarded-for'), getRequestIP(event)]
.filter((v): v is string => typeof v === 'string' && v !== '')
.join(', ')
// Recorded by server/plugins/journal-client-address.ts before any route
// cache hides the request headers.
const forwarded =
(event.context.journalForwardedFor as string | undefined) ||
[getRequestHeader(event, 'x-forwarded-for'), getRequestIP(event)]
.filter((v): v is string => typeof v === 'string' && v !== '')
.join(', ')
const headers: Record<string, string> = forwarded ? { 'x-forwarded-for': forwarded } : {}
const fetcher: JournalFetcher = (path, query) => $fetch(path, { baseURL: base, query, headers })
const api = createJournalClient(fetcher)

View File

@@ -0,0 +1,20 @@
// Keeps the visitor's address available to route-cached SSR renders.
//
// An app rule such as `'/**': { swr }` wraps the page renderer (and any
// matching server route) in Nitro's cached handler, which renders from a copy
// of the request that carries no headers (only `varies` headers survive, so a
// cached page never depends on one visitor's request). Reading
// x-forwarded-for there would forward only the Nuxt peer, and every cache
// miss would share the Journal API's rate-limit bucket. The event context is
// shared with that copy, so the incoming x-forwarded-for plus the peer
// address is recorded on the request hook, before the cached handler runs;
// useJournalApi, /rss.xml and the sitemap source forward it.
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('request', (event) => {
const forwarded = [getRequestHeader(event, 'x-forwarded-for'), event.node.req.socket?.remoteAddress]
.filter((v): v is string => typeof v === 'string' && v !== '')
.join(', ')
event.context.journalForwardedFor = forwarded
})
})

View File

@@ -10,9 +10,13 @@ export default defineEventHandler(async (event) => {
}
const base = (config.journal as { apiBase: string }).apiBase
const forwarded = [getRequestHeader(event, 'x-forwarded-for'), getRequestIP(event)]
.filter((v): v is string => typeof v === 'string' && v !== '')
.join(', ')
// Recorded by server/plugins/journal-client-address.ts before any route
// cache hides the request headers.
const forwarded =
(event.context.journalForwardedFor as string | undefined) ||
[getRequestHeader(event, 'x-forwarded-for'), getRequestIP(event)]
.filter((v): v is string => typeof v === 'string' && v !== '')
.join(', ')
let body: string
try {