Skip to content

Commit 33fcdd0

Browse files
authored
fix(proxy): disable proxying for static deployments (1.x backport) (#877)
1 parent 8254150 commit 33fcdd0

3 files changed

Lines changed: 123 additions & 42 deletions

File tree

‎docs/content/docs/1.guides/2.first-party.md‎

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -236,23 +236,11 @@ PostHog receives the proxy endpoint through SDK config, so it can proxy collecti
236236

237237
### Static Hosting (SSG)
238238

239-
The reverse proxy requires a **server runtime**. A fully static deployment serves the output of [`nuxt generate`](https://nuxt.com/docs/getting-started/prerendering) without a Nitro process to handle `/_scripts/p/**`. Nuxt Scripts warns for known static presets, but it does not rewrite proxy URLs to their third-party origins. Disable proxying for affected scripts or use a host that supports external-origin rewrites. For example, [Vercel rewrites](https://vercel.com/docs/routing/rewrites) accept `/:path*` captures and external destinations:
240-
241-
```json [vercel.json]
242-
{
243-
"rewrites": [
244-
{ "source": "/_scripts/p/www.google-analytics.com/:path*", "destination": "https://www.google-analytics.com/:path*" },
245-
{ "source": "/_scripts/p/www.googletagmanager.com/:path*", "destination": "https://www.googletagmanager.com/:path*" },
246-
{ "source": "/_scripts/p/connect.facebook.net/:path*", "destination": "https://connect.facebook.net/:path*" }
247-
]
248-
}
249-
```
239+
The reverse proxy requires a **server runtime**. A fully static deployment serves the output of [`nuxt generate`](https://nuxt.com/docs/getting-started/prerendering) without a Nitro process to handle `/_scripts/p/**`.
250240

251-
[Netlify proxy rewrites](https://docs.netlify.com/manage/routing/redirects/rewrites-proxies/) use a `200` rule such as `/_scripts/p/www.google-analytics.com/* https://www.google-analytics.com/:splat 200`. Cloudflare Pages is different: its [`_redirects` proxy rules](https://developers.cloudflare.com/pages/configuration/redirects/#proxying) support only relative destinations, not external domains. Use a [Pages Function](https://developers.cloudflare.com/pages/functions/) or Worker if you need this proxy on a static Cloudflare Pages deployment. Only configure domains your site uses; Nuxt DevTools → Scripts and Nitro logs show the registered set.
241+
Nuxt Scripts detects static output (`nuxt generate`, `nuxt build --prerender`, or a static Nitro preset) and disables proxying automatically, with a build warning listing the affected scripts. Scripts still bundle and load from your domain, but their collection requests keep their original third-party URLs and go directly to their origins. Proxy privacy and anonymization do not apply to those direct requests.
252242

253-
::callout{type="warning"}
254-
Platform-level rewrites bypass the privacy anonymization layer. The proxy handler only runs in a Nitro server runtime.
255-
::
243+
To keep requests proxied and anonymized, deploy the server output of `nuxt build` to a host that runs Nitro.
256244

257245
## Proxy Endpoint Security
258246

@@ -387,7 +375,7 @@ The module injects a per-request page token into the SSR payload, so the respons
387375

388376
URL signing requires a server runtime to verify HMAC signatures. Two deployment modes cannot support signing:
389377

390-
**`nuxt generate` (SSG) with static hosting**: Prerendered pages contain proxy URLs, but no Nitro server exists at runtime to verify signatures or forward requests. Proxy endpoints will not work on static hosts such as GitHub Pages. If you need proxy endpoints alongside prerendered pages, deploy to a server target that supports runtime request handling; [Vercel supports both static and server-rendered Nuxt deployments](https://vercel.com/docs/frameworks/full-stack/nuxt).
378+
**`nuxt generate` (SSG) with static hosting**: Static output has no proxy route and no proxy URLs; scripts send their requests directly to their third-party origins. No Nitro server exists at runtime to verify signatures or forward requests. If you need proxy endpoints alongside prerendered pages, deploy to a server target that supports runtime request handling; [Vercel supports both static and server-rendered Nuxt deployments](https://vercel.com/docs/frameworks/full-stack/nuxt).
391379

392380
**`ssr: false` (SPA mode)**: No server-side rendering means no opportunity to sign URLs or embed page tokens. The signing secret lives in server-only runtime config and cannot be accessed from the client. Proxy endpoints still function if deployed with a server, but requests will be unsigned.
393381

@@ -550,7 +538,7 @@ Routing a request through your domain does not settle the consent question. For
550538
| Problem | Fix |
551539
|---------|-----|
552540
| Analytics not tracking | Check DevTools → Network for `/_scripts/p/` requests. Check Nitro server logs for proxy errors |
553-
| Proxy not working on static site | Static hosts do not run the Nitro proxy handler. Disable proxying, add platform rewrites, or switch to a server deployment. See [Static Hosting](#static-hosting-ssg)|
541+
| Proxy not working on static site | Static output disables the proxy automatically and collection requests go direct. Deploy the `nuxt build` server output to enable proxying. See [Static Hosting](#static-hosting-ssg)|
554542
| Stale script | Remove `node_modules/.cache/nuxt/scripts` and rebuild |
555543
| Build download fails | Set `assets.fallbackOnSrcOnBundleFail: true`{lang="ts"} to fall back to direct loading |
556544
| Debugging | Open Nuxt DevTools → Scripts to see proxy routes and privacy status |

‎packages/script/src/module.ts‎

Lines changed: 72 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,42 @@ export function isProxyDisabled(
288288
returnfalse
289289
}
290290

291+
/**
292+
* Nitro presets with no server runtime: `/_scripts/p/**` cannot be served, so
293+
* proxy URLs written into bundled scripts would 404/405 in production.
294+
* Nitro accepts each name in hyphen, underscore, and camelCase form.
295+
*/
296+
exportconstSTATIC_PROXY_PRESETS=[
297+
'static',
298+
'github-pages',
299+
'gitlab-pages',
300+
'cloudflare-pages-static',
301+
'netlify-static',
302+
'vercel-static',
303+
'zeabur-static',
304+
'zerops-static',
305+
]
306+
307+
/**
308+
* Normalize a preset id the way Nitro resolves it: camelCase and underscore
309+
* spellings (`githubPages`, `github_pages`) map to the hyphen form (`github-pages`).
310+
*/
311+
exportfunctionnormalizeNitroPreset(preset: string): string{
312+
returnpreset.replace(/[A-Z]/g,m=>`-${m.toLowerCase()}`).replace(/_/g,'-')
313+
}
314+
315+
/**
316+
* Whether the build targets static output with no Nitro server runtime.
317+
* `nuxi generate` and `nuxt build --prerender` set `_generate` and `nitro.static`
318+
* through CLI overrides; static presets arrive via `nitro.preset`, `NITRO_PRESET`,
319+
* or `SERVER_PRESET` in any of Nitro's accepted spellings.
320+
*/
321+
exportfunctionisStaticProxyTarget(options: {generate?: boolean,nitroStatic?: boolean,preset?: string}): boolean{
322+
return!!options.generate
323+
||options.nitroStatic===true
324+
||STATIC_PROXY_PRESETS.includes(normalizeNitroPreset(options.preset||''))
325+
}
326+
291327
exportfunctionapplyAutoInject(
292328
registry: NuxtConfigScriptRegistry,
293329
runtimeConfig: Record<string,any>,
@@ -740,15 +776,26 @@ export default defineNuxtModule<ModuleOptions>({
740776
__NUXT_SCRIPTS_UNHEAD_SOURCELESS__: unheadSourceLessConst,
741777
}
742778

743-
// Register proxy handler unconditionally. The handler rejects unknown domains
744-
// at runtime, so it's safe to register even when no scripts use proxy.
779+
// Register the proxy handler for server targets. The handler rejects unknown
780+
// domains at runtime, so it's safe to register even when no scripts use proxy.
745781
constscriptsBase=config.prefix||'/_scripts'
746782
constproxyPrefix=`${scriptsBase}/p`
747783
constassetsPrefix=`${scriptsBase}/assets`
748784
constproxyConfigs: Partial<Record<RegistryScriptKey,ProxyConfig>>={}
749785

750786
constproxyHandlerPath=awaitresolvePath('./runtime/server/proxy-handler')
751-
addServerHandler({route: `${proxyPrefix}/**`,handler: proxyHandlerPath})
787+
// Static targets (nuxi generate, static presets) have no server runtime to
788+
// serve the proxy: skip the route so the proxy is fully opt-in for them.
789+
conststaticProxyTarget=isStaticProxyTarget({
790+
// `_generate` arrives untyped through the nuxi generate CLI override. Nitro's
791+
// preset option already merges CLI args, env, and nuxt.config by module setup.
792+
generate: (nuxt.optionsas{_generate?: boolean})._generate,
793+
nitroStatic: (nuxt.options.nitroasany)?.static,
794+
preset: (nuxt.options.nitroasany)?.preset||process.env.NITRO_PRESET||process.env.SERVER_PRESET,
795+
})
796+
if(!staticProxyTarget){
797+
addServerHandler({route: `${proxyPrefix}/**`,handler: proxyHandlerPath})
798+
}
752799

753800
// In dev, sink Vercel Analytics insight POSTs to `/_vercel/insights/*` so
754801
// they don't 404. Vercel's edge serves this path in production; locally
@@ -960,6 +1007,7 @@ export default defineNuxtModule<ModuleOptions>({
9601007
constpartytownScripts=newSet<string>()
9611008

9621009
letanyNeedsProxy=false
1010+
constproxyConfiguredKeys: string[]=[]
9631011
constregistryKeys=Object.keys(config.registry||{})
9641012
for(constkeyofregistryKeys){
9651013
constscript=scriptByKey.get(key)
@@ -975,8 +1023,10 @@ export default defineNuxtModule<ModuleOptions>({
9751023

9761024
constresolved=resolveCapabilities(script,mergedOverrides)
9771025

978-
if(resolved.proxy)
1026+
if(resolved.proxy){
9791027
anyNeedsProxy=true
1028+
proxyConfiguredKeys.push(key)
1029+
}
9801030

9811031
if(resolved.partytown){
9821032
partytownScripts.add(key)
@@ -999,8 +1049,12 @@ export default defineNuxtModule<ModuleOptions>({
9991049
constproxyAlias=config.proxy?.alias
10001050
letdomainAliases: Record<string,string>={}
10011051

1002-
// Finalize proxy setup: build configs, register intercept plugin, wire devtools
1003-
if(anyNeedsProxy){
1052+
// Finalize proxy setup: build configs, register intercept plugin, wire devtools.
1053+
// Static targets (nuxi generate, static presets) have no server runtime to
1054+
// serve `/_scripts/p/**`: skip every proxy integration (AST rewrites, intercept
1055+
// plugin, auto-injected endpoints, URL signing) so collection requests keep
1056+
// their original third-party URLs and work on static hosting.
1057+
if(anyNeedsProxy&&!staticProxyTarget){
10041058
constbuiltConfigs=buildProxyConfigsFromRegistry(registryScripts,scriptByKey)
10051059
Object.assign(proxyConfigs,builtConfigs)
10061060

@@ -1108,17 +1162,6 @@ export default defineNuxtModule<ModuleOptions>({
11081162
logger.success(`Proxy mode enabled for ${registryKeys.length} script(s), ${totalDomains} domain(s) proxied (privacy: ${privacyLabel})`)
11091163
}
11101164

1111-
// Warn for static presets
1112-
constproxyStaticPresets=['static','github-pages','cloudflare-pages-static','netlify-static','azure-static','firebase-static']
1113-
constproxyPreset=process.env.NITRO_PRESET||''
1114-
if(proxyStaticPresets.includes(proxyPreset)){
1115-
logger.warn(
1116-
`Proxy collection endpoints require a server runtime (detected: ${proxyPreset||'static'}).\n`
1117-
+'Scripts will be bundled, but collection requests will not be proxied and URL signing will be unavailable.\n'
1118-
+'Options: configure platform rewrites, switch to server-rendered mode, or disable with proxy: false.',
1119-
)
1120-
}
1121-
11221165
// Expose devtools data
11231166
if(nuxt.options.dev){
11241167
nuxt.options.runtimeConfig.public['nuxt-scripts-devtools']=buildDevtoolsData(proxyPrefix,privacyLabel,devtoolsScripts,aliasToDomain)asany
@@ -1137,6 +1180,13 @@ export default defineNuxtModule<ModuleOptions>({
11371180
}
11381181
}
11391182
}
1183+
elseif(anyNeedsProxy){
1184+
logger.warn(
1185+
`[nuxt-scripts] Static output detected (nuxi generate or a static Nitro preset); the scripts proxy requires a server runtime.\n`
1186+
+`Proxying, its privacy anonymization, and proxy URL signing are disabled for: ${proxyConfiguredKeys.join(', ')}. Scripts still bundle, and their requests go directly to their third-party origins.\n`
1187+
+`Deploy the Nuxt server output (nuxt build) to enable proxying.`,
1188+
)
1189+
}
11401190

11411191
constmoduleInstallPromises: Map<string,()=>Promise<boolean>|undefined>=newMap()
11421192

@@ -1227,10 +1277,7 @@ export default defineNuxtModule<ModuleOptions>({
12271277
)asany
12281278

12291279
// Signing requires a server runtime to verify HMACs. Skip setup entirely
1230-
// for SPA mode or static presets where no Nitro server exists at runtime.
1231-
conststaticPresets=['static','github-pages','cloudflare-pages-static','netlify-static','azure-static','firebase-static']
1232-
constnitroPreset=process.env.NITRO_PRESET||''
1233-
constisStaticTarget=staticPresets.includes(nitroPreset)
1280+
// for SPA mode or static output where no Nitro server exists at runtime.
12341281
constisSpa=nuxt.options.ssr===false
12351282

12361283
// Proxy security explicitly disabled: skip secret resolution and the page
@@ -1240,11 +1287,11 @@ export default defineNuxtModule<ModuleOptions>({
12401287
logger.info('[security] Proxy security disabled via `security: false`. Proxy endpoints will pass requests through without signature verification.')
12411288
}
12421289
}
1243-
elseif(anyHandlerRequiresSigning&&(isSpa||isStaticTarget)){
1290+
elseif(anyHandlerRequiresSigning&&(isSpa||staticProxyTarget)){
12441291
logger.warn(
1245-
`[security] URL signing requires a server runtime${isStaticTarget ? ` (detected preset: ${nitroPreset})` : ' (ssr: false)'}.\n`
1246-
+' Proxy endpoints will work without signature verification.\n'
1247-
+' To enable signing, deploy with a server-rendered target or configure platform-level rewrites.',
1292+
`[security] URL signing requires a server runtime${staticProxyTarget ? ' (static output)' : ' (ssr: false)'}.`
1293+
+'\n Proxy endpoints will work without signature verification.'
1294+
+'\n To enable signing, deploy with a server-rendered target or configure platform-level rewrites.',
12481295
)
12491296
}
12501297
// Resolve the HMAC signing secret only when at least one handler needs it
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import{describe,expect,it}from'vitest'
2+
import{isStaticProxyTarget,normalizeNitroPreset,STATIC_PROXY_PRESETS}from'../../packages/script/src/module'
3+
4+
describe('isStaticProxyTarget',()=>{
5+
it('is false for a regular server build',()=>{
6+
expect(isStaticProxyTarget({generate: false,nitroStatic: false,preset: 'cloudflare_pages'})).toBe(false)
7+
expect(isStaticProxyTarget({})).toBe(false)
8+
expect(isStaticProxyTarget({preset: 'vercel'})).toBe(false)
9+
expect(isStaticProxyTarget({preset: 'netlify'})).toBe(false)
10+
})
11+
12+
it('detects nuxi generate via _generate and nitro.static',()=>{
13+
expect(isStaticProxyTarget({generate: true})).toBe(true)
14+
expect(isStaticProxyTarget({generate: true,preset: 'cloudflare'})).toBe(true)
15+
expect(isStaticProxyTarget({nitroStatic: true})).toBe(true)
16+
})
17+
18+
it('detects every static preset in its hyphen, underscore, and camelCase form',()=>{
19+
expect(isStaticProxyTarget({preset: 'static'})).toBe(true)
20+
expect(isStaticProxyTarget({preset: 'github_pages'})).toBe(true)
21+
expect(isStaticProxyTarget({preset: 'githubPages'})).toBe(true)
22+
expect(isStaticProxyTarget({preset: 'gitlab_pages'})).toBe(true)
23+
expect(isStaticProxyTarget({preset: 'cloudflare_pages_static'})).toBe(true)
24+
expect(isStaticProxyTarget({preset: 'cloudflarePagesStatic'})).toBe(true)
25+
expect(isStaticProxyTarget({preset: 'netlify_static'})).toBe(true)
26+
expect(isStaticProxyTarget({preset: 'vercel_static'})).toBe(true)
27+
expect(isStaticProxyTarget({preset: 'zeabur_static'})).toBe(true)
28+
expect(isStaticProxyTarget({preset: 'zerops_static'})).toBe(true)
29+
})
30+
31+
it('keeps the preset list free of non-static or unknown presets',()=>{
32+
// azure-static and firebase-static are not Nitro presets; gitlab-pages is.
33+
expect(STATIC_PROXY_PRESETS).not.toContain('azure-static')
34+
expect(STATIC_PROXY_PRESETS).not.toContain('firebase-static')
35+
expect(STATIC_PROXY_PRESETS).toContain('gitlab-pages')
36+
})
37+
})
38+
39+
describe('normalizeNitroPreset',()=>{
40+
it('maps underscore and camelCase spellings onto the hyphen form',()=>{
41+
expect(normalizeNitroPreset('github_pages')).toBe('github-pages')
42+
expect(normalizeNitroPreset('githubPages')).toBe('github-pages')
43+
expect(normalizeNitroPreset('github-pages')).toBe('github-pages')
44+
expect(normalizeNitroPreset('static')).toBe('static')
45+
})
46+
})

0 commit comments

Comments
 (0)