Skip to content

Commit ba5f2c9

Browse files
authored
fix(proxy): disable proxying for static deployments (#876)
1 parent 625af07 commit ba5f2c9

3 files changed

Lines changed: 117 additions & 33 deletions

File tree

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

Lines changed: 4 additions & 16 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

@@ -421,7 +409,7 @@ Routing a request through your domain does not settle the consent question. For
421409
| Problem | Fix |
422410
|---------|-----|
423411
| Analytics not tracking | Check DevTools → Network for `/_scripts/p/` requests. Check Nitro server logs for proxy errors |
424-
| 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)|
412+
| 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)|
425413
| Stale script | Remove `node_modules/.cache/nuxt/scripts` and rebuild |
426414
| Build download fails | Set `assets.fallbackOnSrcOnBundleFail: true`{lang="ts"} to fall back to direct loading |
427415
| Debugging | Open Nuxt DevTools → Scripts to see proxy routes and privacy status |

‎packages/script/src/module.ts‎

Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,42 @@ export function isProxyDisabled(
8686
returnfalse
8787
}
8888

89+
/**
90+
* Nitro presets with no server runtime: `/_scripts/p/**` cannot be served, so
91+
* proxy URLs written into bundled scripts would 404/405 in production.
92+
* Nitro accepts each name in hyphen, underscore, and camelCase form.
93+
*/
94+
exportconstSTATIC_PROXY_PRESETS=[
95+
'static',
96+
'github-pages',
97+
'gitlab-pages',
98+
'cloudflare-pages-static',
99+
'netlify-static',
100+
'vercel-static',
101+
'zeabur-static',
102+
'zerops-static',
103+
]
104+
105+
/**
106+
* Normalize a preset id the way Nitro resolves it: camelCase and underscore
107+
* spellings (`githubPages`, `github_pages`) map to the hyphen form (`github-pages`).
108+
*/
109+
exportfunctionnormalizeNitroPreset(preset: string): string{
110+
returnpreset.replace(/[A-Z]/g,m=>`-${m.toLowerCase()}`).replace(/_/g,'-')
111+
}
112+
113+
/**
114+
* Whether the build targets static output with no Nitro server runtime.
115+
* `nuxi generate` and `nuxt build --prerender` set `_generate` and `nitro.static`
116+
* through CLI overrides; static presets arrive via `nitro.preset`, `NITRO_PRESET`,
117+
* or `SERVER_PRESET` in any of Nitro's accepted spellings.
118+
*/
119+
exportfunctionisStaticProxyTarget(options: {generate?: boolean,nitroStatic?: boolean,preset?: string}): boolean{
120+
return!!options.generate
121+
||options.nitroStatic===true
122+
||STATIC_PROXY_PRESETS.includes(normalizeNitroPreset(options.preset||''))
123+
}
124+
89125
exportfunctionapplyAutoInject(
90126
registry: NuxtConfigScriptRegistry,
91127
runtimeConfig: Record<string,any>,
@@ -437,15 +473,26 @@ export default defineNuxtModule<ModuleOptions>({
437473
__NUXT_SCRIPTS_DEBUG__: debugConst,
438474
}
439475

440-
// Register proxy handler unconditionally. The handler rejects unknown domains
441-
// at runtime, so it's safe to register even when no scripts use proxy.
476+
// Register the proxy handler for server targets. The handler rejects unknown
477+
// domains at runtime, so it's safe to register even when no scripts use proxy.
442478
constscriptsBase=config.prefix||'/_scripts'
443479
constproxyPrefix=`${scriptsBase}/p`
444480
constassetsPrefix=`${scriptsBase}/assets`
445481
constproxyConfigs: Partial<Record<RegistryScriptKey,ProxyConfig>>={}
446482

447483
constproxyHandlerPath=awaitresolvePath('./runtime/server/proxy-handler')
448-
addServerHandler({route: `${proxyPrefix}/**`,handler: proxyHandlerPath})
484+
// Static targets (nuxi generate, static presets) have no server runtime to
485+
// serve the proxy: skip the route so the proxy is fully opt-in for them.
486+
conststaticProxyTarget=isStaticProxyTarget({
487+
// `_generate` arrives untyped through the nuxi generate CLI override. Nitro's
488+
// preset option already merges CLI args, env, and nuxt.config by module setup.
489+
generate: (nuxt.optionsas{_generate?: boolean})._generate,
490+
nitroStatic: (nuxt.options.nitroasany)?.static,
491+
preset: (nuxt.options.nitroasany)?.preset||process.env.NITRO_PRESET||process.env.SERVER_PRESET,
492+
})
493+
if(!staticProxyTarget){
494+
addServerHandler({route: `${proxyPrefix}/**`,handler: proxyHandlerPath})
495+
}
449496

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

650697
letanyNeedsProxy=false
698+
constproxyConfiguredKeys: string[]=[]
651699
constregistryKeys=Object.keys(config.registry||{})
652700
for(constkeyofregistryKeys){
653701
constscript=scriptByKey.get(key)
@@ -663,8 +711,10 @@ export default defineNuxtModule<ModuleOptions>({
663711

664712
constresolved=resolveCapabilities(script,mergedOverrides)
665713

666-
if(resolved.proxy)
714+
if(resolved.proxy){
667715
anyNeedsProxy=true
716+
proxyConfiguredKeys.push(key)
717+
}
668718

669719
if(resolved.partytown){
670720
partytownScripts.add(key)
@@ -687,8 +737,12 @@ export default defineNuxtModule<ModuleOptions>({
687737
constproxyAlias=config.proxy?.alias
688738
letdomainAliases: Record<string,string>={}
689739

690-
// Finalize proxy setup: build configs, register intercept plugin, wire devtools
691-
if(anyNeedsProxy){
740+
// Finalize proxy setup: build configs, register intercept plugin, wire devtools.
741+
// Static targets (nuxi generate, static presets) have no server runtime to
742+
// serve `/_scripts/p/**`: skip every proxy integration (AST rewrites, intercept
743+
// plugin, auto-injected endpoints) so collection requests keep their original
744+
// third-party URLs and work on static hosting.
745+
if(anyNeedsProxy&&!staticProxyTarget){
692746
constbuiltConfigs=buildProxyConfigsFromRegistry(registryScripts,scriptByKey)
693747
Object.assign(proxyConfigs,builtConfigs)
694748

@@ -796,17 +850,6 @@ export default defineNuxtModule<ModuleOptions>({
796850
logger.success(`Proxy mode enabled for ${registryKeys.length} script(s), ${totalDomains} domain(s) proxied (privacy: ${privacyLabel})`)
797851
}
798852

799-
// Warn for static presets
800-
constproxyStaticPresets=['static','github-pages','cloudflare-pages-static','netlify-static','azure-static','firebase-static']
801-
constproxyPreset=process.env.NITRO_PRESET||''
802-
if(proxyStaticPresets.includes(proxyPreset)){
803-
logger.warn(
804-
`Proxy collection endpoints require a server runtime (detected: ${proxyPreset||'static'}).\n`
805-
+'Scripts will be bundled, but collection requests will not be proxied.\n'
806-
+'Options: configure platform rewrites, switch to server-rendered mode, or disable with proxy: false.',
807-
)
808-
}
809-
810853
// Expose devtools data
811854
if(nuxt.options.dev){
812855
nuxt.options.runtimeConfig.public['nuxt-scripts-devtools']=buildDevtoolsData(proxyPrefix,privacyLabel,devtoolsScripts,aliasToDomain)asany
@@ -825,6 +868,13 @@ export default defineNuxtModule<ModuleOptions>({
825868
}
826869
}
827870
}
871+
elseif(anyNeedsProxy){
872+
logger.warn(
873+
`[nuxt-scripts] Static output detected (nuxi generate or a static Nitro preset); the scripts proxy requires a server runtime.\n`
874+
+`Proxying and its privacy anonymization are disabled for: ${proxyConfiguredKeys.join(', ')}. Scripts still bundle, and their requests go directly to their third-party origins.\n`
875+
+`Deploy the Nuxt server output (nuxt build) to enable proxying.`,
876+
)
877+
}
828878

829879
constmoduleInstallPromises: Map<string,()=>Promise<boolean>|undefined>=newMap()
830880

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)