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:
@@ -112,7 +112,7 @@ Runtime config, overridable by environment variables at start:
|
||||
|
||||
The module also copies `routes`, `perPage`, `sort`, `relatedCount` and `rss` into `runtimeConfig.public.journal`.
|
||||
|
||||
Server-side requests forward the visitor's `x-forwarded-for` and append the address of the peer that reached Nuxt. The Journal API rate-limits anonymous reads per client address; with a trusted proxy configured for the Nuxt server's address, each visitor keeps their own bucket instead of all SSR traffic sharing one. The API base always comes from runtime config, never from request input.
|
||||
Server-side requests forward the visitor's `x-forwarded-for` and append the address of the peer that reached Nuxt. The Journal API rate-limits anonymous reads per client address; with a trusted proxy configured for the Nuxt server's address, each visitor keeps their own bucket instead of all SSR traffic sharing one. Under route caching (`swr` or `cache` route rules), Nitro renders a page from a copy of the request that carries no headers, so the layer's Nitro plugin `server/plugins/journal-client-address.ts` records the address in `event.context.journalForwardedFor` on the request hook, and `useJournalApi`, `/rss.xml` and the sitemap source forward that value. The API base always comes from runtime config, never from request input.
|
||||
|
||||
## Site cache purge
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// the internal base (runtimeConfig.journal.apiBase) and forwards the visitor's
|
||||
// x-forwarded-for, plus the address of the peer that reached Nuxt, so the Go
|
||||
// rate limiter keeps one bucket per visitor instead of one for the Nuxt
|
||||
// server. In the browser it calls the same-origin public base. The base is
|
||||
// server, also on route-cached renders. In the browser it calls the same-origin public base. The base is
|
||||
// always runtime config, never request input.
|
||||
|
||||
import { createJournalClient, type JournalClient, type JournalFetcher } from '../utils/client.ts'
|
||||
@@ -12,9 +12,13 @@ export function useJournalApi(): JournalClient {
|
||||
|
||||
if (import.meta.server) {
|
||||
const base = (config.journal as { apiBase: string }).apiBase
|
||||
// Under route caching the request copy has no headers; the address the
|
||||
// layer's server/plugins/journal-client-address.ts recorded comes first.
|
||||
const event = useRequestEvent()
|
||||
const recorded = event?.context.journalForwardedFor as string | undefined
|
||||
const incoming = useRequestHeaders(['x-forwarded-for'])['x-forwarded-for']
|
||||
const peer = useRequestEvent()?.node?.req?.socket?.remoteAddress
|
||||
const forwarded = [incoming, peer].filter((v): v is string => typeof v === 'string' && v !== '').join(', ')
|
||||
const peer = event?.node?.req?.socket?.remoteAddress
|
||||
const forwarded = recorded || [incoming, peer].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 })
|
||||
return createJournalClient(fetcher)
|
||||
|
||||
@@ -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)
|
||||
|
||||
20
server/plugins/journal-client-address.ts
Normal file
20
server/plugins/journal-client-address.ts
Normal 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
|
||||
})
|
||||
})
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user