Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 92
fix(proxy): disable proxying for static deployments (1.x backport)#877
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Uh oh!
There was an error while loading. Please reload this page.
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -288,6 +288,42 @@ export function isProxyDisabled( | ||
| return false | ||
| } | ||
| /** | ||
| * Nitro presets with no server runtime: `/_scripts/p/**` cannot be served, so | ||
| * proxy URLs written into bundled scripts would 404/405 in production. | ||
| * Nitro accepts each name in hyphen, underscore, and camelCase form. | ||
| */ | ||
| export const STATIC_PROXY_PRESETS = [ | ||
| 'static', | ||
| 'github-pages', | ||
| 'gitlab-pages', | ||
| 'cloudflare-pages-static', | ||
| 'netlify-static', | ||
| 'vercel-static', | ||
| 'zeabur-static', | ||
| 'zerops-static', | ||
| ] | ||
| /** | ||
| * Normalize a preset id the way Nitro resolves it: camelCase and underscore | ||
| * spellings (`githubPages`, `github_pages`) map to the hyphen form (`github-pages`). | ||
| */ | ||
| export function normalizeNitroPreset(preset: string): string { | ||
| return preset.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`).replace(/_/g, '-') | ||
| } | ||
| /** | ||
| * Whether the build targets static output with no Nitro server runtime. | ||
| * `nuxi generate` and `nuxt build --prerender` set `_generate` and `nitro.static` | ||
| * through CLI overrides; static presets arrive via `nitro.preset`, `NITRO_PRESET`, | ||
| * or `SERVER_PRESET` in any of Nitro's accepted spellings. | ||
| */ | ||
| export function isStaticProxyTarget(options: { generate?: boolean, nitroStatic?: boolean, preset?: string }): boolean { | ||
| return !!options.generate | ||
| || options.nitroStatic === true | ||
| || STATIC_PROXY_PRESETS.includes(normalizeNitroPreset(options.preset || '')) | ||
| } | ||
| export function applyAutoInject( | ||
| registry: NuxtConfigScriptRegistry, | ||
| runtimeConfig: Record<string, any>, | ||
| @@ -740,15 +776,26 @@ export default defineNuxtModule<ModuleOptions>({ | ||
| __NUXT_SCRIPTS_UNHEAD_SOURCELESS__: unheadSourceLessConst, | ||
| } | ||
| // Register proxy handler unconditionally. The handler rejects unknown domains | ||
| // at runtime, so it's safe to register even when no scripts use proxy. | ||
| // Register the proxy handler for server targets. The handler rejects unknown | ||
| // domains at runtime, so it's safe to register even when no scripts use proxy. | ||
| const scriptsBase = config.prefix || '/_scripts' | ||
| const proxyPrefix = `${scriptsBase}/p` | ||
| const assetsPrefix = `${scriptsBase}/assets` | ||
| const proxyConfigs: Partial<Record<RegistryScriptKey, ProxyConfig>> = {} | ||
| const proxyHandlerPath = await resolvePath('./runtime/server/proxy-handler') | ||
| addServerHandler({ route: `${proxyPrefix}/**`, handler: proxyHandlerPath }) | ||
| // Static targets (nuxi generate, static presets) have no server runtime to | ||
| // serve the proxy: skip the route so the proxy is fully opt-in for them. | ||
| const staticProxyTarget = isStaticProxyTarget({ | ||
| // `_generate` arrives untyped through the nuxi generate CLI override. Nitro's | ||
| // preset option already merges CLI args, env, and nuxt.config by module setup. | ||
| generate: (nuxt.options as { _generate?: boolean })._generate, | ||
| nitroStatic: (nuxt.options.nitro as any)?.static, | ||
| preset: (nuxt.options.nitro as any)?.preset || process.env.NITRO_PRESET || process.env.SERVER_PRESET, | ||
| }) | ||
coderabbitai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| if (!staticProxyTarget) { | ||
| addServerHandler({ route: `${proxyPrefix}/**`, handler: proxyHandlerPath }) | ||
| } | ||
| // In dev, sink Vercel Analytics insight POSTs to `/_vercel/insights/*` so | ||
| // they don't 404. Vercel's edge serves this path in production; locally | ||
| @@ -960,6 +1007,7 @@ export default defineNuxtModule<ModuleOptions>({ | ||
| const partytownScripts = new Set<string>() | ||
| let anyNeedsProxy = false | ||
| const proxyConfiguredKeys: string[] = [] | ||
| const registryKeys = Object.keys(config.registry || {}) | ||
| for (const key of registryKeys) { | ||
| const script = scriptByKey.get(key) | ||
| @@ -975,8 +1023,10 @@ export default defineNuxtModule<ModuleOptions>({ | ||
| const resolved = resolveCapabilities(script, mergedOverrides) | ||
| if (resolved.proxy) | ||
| if (resolved.proxy) { | ||
| anyNeedsProxy = true | ||
| proxyConfiguredKeys.push(key) | ||
| } | ||
| if (resolved.partytown) { | ||
| partytownScripts.add(key) | ||
| @@ -999,8 +1049,12 @@ export default defineNuxtModule<ModuleOptions>({ | ||
| const proxyAlias = config.proxy?.alias | ||
| let domainAliases: Record<string, string> = {} | ||
| // Finalize proxy setup: build configs, register intercept plugin, wire devtools | ||
| if (anyNeedsProxy) { | ||
| // Finalize proxy setup: build configs, register intercept plugin, wire devtools. | ||
| // Static targets (nuxi generate, static presets) have no server runtime to | ||
| // serve `/_scripts/p/**`: skip every proxy integration (AST rewrites, intercept | ||
| // plugin, auto-injected endpoints, URL signing) so collection requests keep | ||
| // their original third-party URLs and work on static hosting. | ||
| if (anyNeedsProxy && !staticProxyTarget) { | ||
| const builtConfigs = buildProxyConfigsFromRegistry(registryScripts, scriptByKey) | ||
| Object.assign(proxyConfigs, builtConfigs) | ||
| @@ -1108,17 +1162,6 @@ export default defineNuxtModule<ModuleOptions>({ | ||
| logger.success(`Proxy mode enabled for ${registryKeys.length} script(s), ${totalDomains} domain(s) proxied (privacy: ${privacyLabel})`) | ||
| } | ||
| // Warn for static presets | ||
| const proxyStaticPresets = ['static', 'github-pages', 'cloudflare-pages-static', 'netlify-static', 'azure-static', 'firebase-static'] | ||
| const proxyPreset = process.env.NITRO_PRESET || '' | ||
| if (proxyStaticPresets.includes(proxyPreset)) { | ||
| logger.warn( | ||
| `Proxy collection endpoints require a server runtime (detected: ${proxyPreset || 'static'}).\n` | ||
| + 'Scripts will be bundled, but collection requests will not be proxied and URL signing will be unavailable.\n' | ||
| + 'Options: configure platform rewrites, switch to server-rendered mode, or disable with proxy: false.', | ||
| ) | ||
| } | ||
| // Expose devtools data | ||
| if (nuxt.options.dev) { | ||
| nuxt.options.runtimeConfig.public['nuxt-scripts-devtools'] = buildDevtoolsData(proxyPrefix, privacyLabel, devtoolsScripts, aliasToDomain) as any | ||
| @@ -1137,6 +1180,13 @@ export default defineNuxtModule<ModuleOptions>({ | ||
| } | ||
| } | ||
| } | ||
| else if (anyNeedsProxy) { | ||
| logger.warn( | ||
| `[nuxt-scripts] Static output detected (nuxi generate or a static Nitro preset); the scripts proxy requires a server runtime.\n` | ||
| + `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` | ||
| + `Deploy the Nuxt server output (nuxt build) to enable proxying.`, | ||
| ) | ||
| } | ||
| const moduleInstallPromises: Map<string, () => Promise<boolean> | undefined> = new Map() | ||
| @@ -1227,10 +1277,7 @@ export default defineNuxtModule<ModuleOptions>({ | ||
| ) as any | ||
| // Signing requires a server runtime to verify HMACs. Skip setup entirely | ||
| // for SPA mode or static presets where no Nitro server exists at runtime. | ||
| const staticPresets = ['static', 'github-pages', 'cloudflare-pages-static', 'netlify-static', 'azure-static', 'firebase-static'] | ||
| const nitroPreset = process.env.NITRO_PRESET || '' | ||
| const isStaticTarget = staticPresets.includes(nitroPreset) | ||
| // for SPA mode or static output where no Nitro server exists at runtime. | ||
| const isSpa = nuxt.options.ssr === false | ||
| // Proxy security explicitly disabled: skip secret resolution and the page | ||
| @@ -1240,11 +1287,11 @@ export default defineNuxtModule<ModuleOptions>({ | ||
| logger.info('[security] Proxy security disabled via `security: false`. Proxy endpoints will pass requests through without signature verification.') | ||
| } | ||
| } | ||
| else if (anyHandlerRequiresSigning && (isSpa || isStaticTarget)) { | ||
| else if (anyHandlerRequiresSigning && (isSpa || staticProxyTarget)) { | ||
| logger.warn( | ||
| `[security] URL signing requires a server runtime${isStaticTarget ? ` (detected preset: ${nitroPreset})` : ' (ssr: false)'}.\n` | ||
| + ' Proxy endpoints will work without signature verification.\n' | ||
| + ' To enable signing, deploy with a server-rendered target or configure platform-level rewrites.', | ||
| `[security] URL signing requires a server runtime${staticProxyTarget ? ' (static output)' : ' (ssr: false)'}.` | ||
| + '\n Proxy endpoints will work without signature verification.' | ||
| + '\n To enable signing, deploy with a server-rendered target or configure platform-level rewrites.', | ||
| ) | ||
| } | ||
| // Resolve the HMAC signing secret only when at least one handler needs it | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { describe, expect, it } from 'vitest' | ||
| import { isStaticProxyTarget, normalizeNitroPreset, STATIC_PROXY_PRESETS } from '../../packages/script/src/module' | ||
| describe('isStaticProxyTarget', () => { | ||
| it('is false for a regular server build', () => { | ||
| expect(isStaticProxyTarget({ generate: false, nitroStatic: false, preset: 'cloudflare_pages' })).toBe(false) | ||
| expect(isStaticProxyTarget({})).toBe(false) | ||
| expect(isStaticProxyTarget({ preset: 'vercel' })).toBe(false) | ||
| expect(isStaticProxyTarget({ preset: 'netlify' })).toBe(false) | ||
| }) | ||
| it('detects nuxi generate via _generate and nitro.static', () => { | ||
| expect(isStaticProxyTarget({ generate: true })).toBe(true) | ||
| expect(isStaticProxyTarget({ generate: true, preset: 'cloudflare' })).toBe(true) | ||
| expect(isStaticProxyTarget({ nitroStatic: true })).toBe(true) | ||
| }) | ||
| it('detects every static preset in its hyphen, underscore, and camelCase form', () => { | ||
| expect(isStaticProxyTarget({ preset: 'static' })).toBe(true) | ||
| expect(isStaticProxyTarget({ preset: 'github_pages' })).toBe(true) | ||
| expect(isStaticProxyTarget({ preset: 'githubPages' })).toBe(true) | ||
| expect(isStaticProxyTarget({ preset: 'gitlab_pages' })).toBe(true) | ||
| expect(isStaticProxyTarget({ preset: 'cloudflare_pages_static' })).toBe(true) | ||
| expect(isStaticProxyTarget({ preset: 'cloudflarePagesStatic' })).toBe(true) | ||
| expect(isStaticProxyTarget({ preset: 'netlify_static' })).toBe(true) | ||
| expect(isStaticProxyTarget({ preset: 'vercel_static' })).toBe(true) | ||
| expect(isStaticProxyTarget({ preset: 'zeabur_static' })).toBe(true) | ||
| expect(isStaticProxyTarget({ preset: 'zerops_static' })).toBe(true) | ||
| }) | ||
| it('keeps the preset list free of non-static or unknown presets', () => { | ||
| // azure-static and firebase-static are not Nitro presets; gitlab-pages is. | ||
| expect(STATIC_PROXY_PRESETS).not.toContain('azure-static') | ||
| expect(STATIC_PROXY_PRESETS).not.toContain('firebase-static') | ||
| expect(STATIC_PROXY_PRESETS).toContain('gitlab-pages') | ||
| }) | ||
| }) | ||
| describe('normalizeNitroPreset', () => { | ||
| it('maps underscore and camelCase spellings onto the hyphen form', () => { | ||
| expect(normalizeNitroPreset('github_pages')).toBe('github-pages') | ||
| expect(normalizeNitroPreset('githubPages')).toBe('github-pages') | ||
| expect(normalizeNitroPreset('github-pages')).toBe('github-pages') | ||
| expect(normalizeNitroPreset('static')).toBe('static') | ||
| }) | ||
| }) |
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.