From 03525cb1117a31124ecc341d442e522ee35d3725 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 3 Nov 2025 12:58:48 +0100 Subject: [PATCH 1/3] fix wrapping --- .../templates/middlewareWrapperTemplate.ts | 14 +- .../nextjs/test/config/wrappingLoader.test.ts | 155 ++++++++++++++++++ 2 files changed, 165 insertions(+), 4 deletions(-) diff --git a/packages/nextjs/src/config/templates/middlewareWrapperTemplate.ts b/packages/nextjs/src/config/templates/middlewareWrapperTemplate.ts index 236f4eff3999..330756859dff 100644 --- a/packages/nextjs/src/config/templates/middlewareWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/middlewareWrapperTemplate.ts @@ -26,13 +26,17 @@ const userApiModule = origModule as NextApiModule; // the case Next.js wil crash during runtime but the Sentry SDK should definitely not crash so we need tohandle it. let userProvidedNamedHandler: EdgeRouteHandler | undefined = undefined; let userProvidedDefaultHandler: EdgeRouteHandler | undefined = undefined; +let userProvidedMiddleware = false; +let userProvidedProxy = false; if ('middleware' in userApiModule && typeof userApiModule.middleware === 'function') { // Handle when user defines via named ESM export: `export { middleware };` userProvidedNamedHandler = userApiModule.middleware; + userProvidedMiddleware = true; } else if ('proxy' in userApiModule && typeof userApiModule.proxy === 'function') { // Handle when user defines via named ESM export (Next.js 16): `export { proxy };` userProvidedNamedHandler = userApiModule.proxy; + userProvidedProxy = true; } else if ('default' in userApiModule && typeof userApiModule.default === 'function') { // Handle when user defines via ESM export: `export default myFunction;` userProvidedDefaultHandler = userApiModule.default; @@ -41,10 +45,12 @@ if ('middleware' in userApiModule && typeof userApiModule.middleware === 'functi userProvidedDefaultHandler = userApiModule; } -export const middleware = userProvidedNamedHandler - ? Sentry.wrapMiddlewareWithSentry(userProvidedNamedHandler) - : undefined; -export const proxy = userProvidedNamedHandler ? Sentry.wrapMiddlewareWithSentry(userProvidedNamedHandler) : undefined; +export const middleware = + userProvidedMiddleware && userProvidedNamedHandler + ? Sentry.wrapMiddlewareWithSentry(userProvidedNamedHandler) + : undefined; +export const proxy = + userProvidedProxy && userProvidedNamedHandler ? Sentry.wrapMiddlewareWithSentry(userProvidedNamedHandler) : undefined; export default userProvidedDefaultHandler ? Sentry.wrapMiddlewareWithSentry(userProvidedDefaultHandler) : undefined; // Re-export anything exported by the page module we're wrapping. When processing this code, Rollup is smart enough to diff --git a/packages/nextjs/test/config/wrappingLoader.test.ts b/packages/nextjs/test/config/wrappingLoader.test.ts index 7f6e5f3a3c66..f26032eeb6c5 100644 --- a/packages/nextjs/test/config/wrappingLoader.test.ts +++ b/packages/nextjs/test/config/wrappingLoader.test.ts @@ -102,4 +102,159 @@ describe('wrappingLoader', () => { expect(callback).toHaveBeenCalledWith(null, expect.stringContaining("'/my/route'"), expect.anything()); }); + + describe('middleware wrapping', () => { + it('should only export "proxy" when user exports named "proxy" export', async () => { + const callback = vi.fn(); + + const userCode = ` + export function proxy(request) { + return new Response('ok'); + } + `; + const userCodeSourceMap = undefined; + + const loaderPromise = new Promise(resolve => { + const loaderThis = { + ...defaultLoaderThis, + resourcePath: '/my/src/proxy.ts', + callback: callback.mockImplementation(() => { + resolve(); + }), + getOptions() { + return { + pagesDir: '/my/pages', + appDir: '/my/app', + pageExtensionRegex: DEFAULT_PAGE_EXTENSION_REGEX, + excludeServerRoutes: [], + wrappingTargetKind: 'middleware', + vercelCronsConfig: undefined, + nextjsRequestAsyncStorageModulePath: '/my/request-async-storage.js', + }; + }, + } satisfies LoaderThis; + + wrappingLoader.call(loaderThis, userCode, userCodeSourceMap); + }); + + await loaderPromise; + + const wrappedCode = callback.mock.calls[0][1]; + + // Verify both exports are present in the final export statement + expect(wrappedCode).toMatch(/export \{[^}]*\bmiddleware\b[^}]*\bproxy\b[^}]*\}/); + + // Should wrap proxy (userProvidedProxy should be true) + expect(wrappedCode).toContain('userProvidedProxy = true'); + expect(wrappedCode).toContain('const proxy = userProvidedProxy && userProvidedNamedHandler'); + + // Should NOT wrap middleware (userProvidedMiddleware should remain false) + expect(wrappedCode).toContain('userProvidedMiddleware = false'); + expect(wrappedCode).toContain('const middleware = userProvidedMiddleware && userProvidedNamedHandler'); + + // Verify wrapMiddlewareWithSentry is called in the ternary + expect(wrappedCode).toContain('Sentry.wrapMiddlewareWithSentry(userProvidedNamedHandler)'); + }); + + it('should only export "middleware" when user exports named "middleware" export', async () => { + const callback = vi.fn(); + + const userCode = ` + export function middleware(request) { + return new Response('ok'); + } + `; + const userCodeSourceMap = undefined; + + const loaderPromise = new Promise(resolve => { + const loaderThis = { + ...defaultLoaderThis, + resourcePath: '/my/src/middleware.ts', + callback: callback.mockImplementation(() => { + resolve(); + }), + getOptions() { + return { + pagesDir: '/my/pages', + appDir: '/my/app', + pageExtensionRegex: DEFAULT_PAGE_EXTENSION_REGEX, + excludeServerRoutes: [], + wrappingTargetKind: 'middleware', + vercelCronsConfig: undefined, + nextjsRequestAsyncStorageModulePath: '/my/request-async-storage.js', + }; + }, + } satisfies LoaderThis; + + wrappingLoader.call(loaderThis, userCode, userCodeSourceMap); + }); + + await loaderPromise; + + const wrappedCode = callback.mock.calls[0][1]; + + // Verify both exports are present in the final export statement + expect(wrappedCode).toMatch(/export \{[^}]*\bmiddleware\b[^}]*\bproxy\b[^}]*\}/); + + // Should wrap middleware (userProvidedMiddleware should be true) + expect(wrappedCode).toContain('userProvidedMiddleware = true'); + expect(wrappedCode).toContain('const middleware = userProvidedMiddleware && userProvidedNamedHandler'); + + // Should NOT wrap proxy (userProvidedProxy should remain false) + expect(wrappedCode).toContain('userProvidedProxy = false'); + expect(wrappedCode).toContain('const proxy = userProvidedProxy && userProvidedNamedHandler'); + + // Verify wrapMiddlewareWithSentry is called in the ternary + expect(wrappedCode).toContain('Sentry.wrapMiddlewareWithSentry(userProvidedNamedHandler)'); + }); + + it('should not export named exports when user only exports default', async () => { + const callback = vi.fn(); + + const userCode = ` + export default function(request) { + return new Response('ok'); + } + `; + const userCodeSourceMap = undefined; + + const loaderPromise = new Promise(resolve => { + const loaderThis = { + ...defaultLoaderThis, + resourcePath: '/my/src/middleware.ts', + callback: callback.mockImplementation(() => { + resolve(); + }), + getOptions() { + return { + pagesDir: '/my/pages', + appDir: '/my/app', + pageExtensionRegex: DEFAULT_PAGE_EXTENSION_REGEX, + excludeServerRoutes: [], + wrappingTargetKind: 'middleware', + vercelCronsConfig: undefined, + nextjsRequestAsyncStorageModulePath: '/my/request-async-storage.js', + }; + }, + } satisfies LoaderThis; + + wrappingLoader.call(loaderThis, userCode, userCodeSourceMap); + }); + + await loaderPromise; + + const wrappedCode = callback.mock.calls[0][1]; + + // Should export default + expect(wrappedCode).toMatch(/export \{[^}]* as default[^}]*\}/); + + // Both flags should be false (no named exports provided by user) + expect(wrappedCode).toContain('userProvidedMiddleware = false'); + expect(wrappedCode).toContain('userProvidedProxy = false'); + + // Both named exports should evaluate to undefined (false && handler = undefined) + expect(wrappedCode).toMatch(/const middleware = userProvidedMiddleware && userProvidedNamedHandler/); + expect(wrappedCode).toMatch(/const proxy = userProvidedProxy && userProvidedNamedHandler/); + }); + }); }); From 548ff07bb86c33b982343d2d6b998abaddee3bae Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 3 Nov 2025 17:32:16 +0100 Subject: [PATCH 2/3] fix template --- .../config/templates/middlewareWrapperTemplate.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/nextjs/src/config/templates/middlewareWrapperTemplate.ts b/packages/nextjs/src/config/templates/middlewareWrapperTemplate.ts index 330756859dff..30c77cf32952 100644 --- a/packages/nextjs/src/config/templates/middlewareWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/middlewareWrapperTemplate.ts @@ -45,12 +45,14 @@ if ('middleware' in userApiModule && typeof userApiModule.middleware === 'functi userProvidedDefaultHandler = userApiModule; } -export const middleware = - userProvidedMiddleware && userProvidedNamedHandler - ? Sentry.wrapMiddlewareWithSentry(userProvidedNamedHandler) - : undefined; -export const proxy = - userProvidedProxy && userProvidedNamedHandler ? Sentry.wrapMiddlewareWithSentry(userProvidedNamedHandler) : undefined; +// Wrap the handler that the user provided (middleware, proxy, or default) +// We preserve the original export names so Next.js can handle its internal renaming logic +const wrappedHandler = userProvidedNamedHandler ? Sentry.wrapMiddlewareWithSentry(userProvidedNamedHandler) : undefined; + +// Only export the named export that the user actually provided +// This ensures Next.js sees the same export structure and can apply its renaming logic +export const middleware = userProvidedMiddleware ? wrappedHandler : undefined; +export const proxy = userProvidedProxy ? wrappedHandler : undefined; export default userProvidedDefaultHandler ? Sentry.wrapMiddlewareWithSentry(userProvidedDefaultHandler) : undefined; // Re-export anything exported by the page module we're wrapping. When processing this code, Rollup is smart enough to From 2cae41d92f957cd95356cce59415de53baae5246 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 3 Nov 2025 17:40:55 +0100 Subject: [PATCH 3/3] fix tests --- .../nextjs/test/config/wrappingLoader.test.ts | 40 ++++++++----------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/packages/nextjs/test/config/wrappingLoader.test.ts b/packages/nextjs/test/config/wrappingLoader.test.ts index f26032eeb6c5..ab33450790bb 100644 --- a/packages/nextjs/test/config/wrappingLoader.test.ts +++ b/packages/nextjs/test/config/wrappingLoader.test.ts @@ -104,7 +104,7 @@ describe('wrappingLoader', () => { }); describe('middleware wrapping', () => { - it('should only export "proxy" when user exports named "proxy" export', async () => { + it('should export proxy when user exports named "proxy" export', async () => { const callback = vi.fn(); const userCode = ` @@ -141,22 +141,18 @@ describe('wrappingLoader', () => { const wrappedCode = callback.mock.calls[0][1]; - // Verify both exports are present in the final export statement + // Verify both exports are present in export statement (Rollup bundles this way) expect(wrappedCode).toMatch(/export \{[^}]*\bmiddleware\b[^}]*\bproxy\b[^}]*\}/); - // Should wrap proxy (userProvidedProxy should be true) + // Should detect proxy export expect(wrappedCode).toContain('userProvidedProxy = true'); - expect(wrappedCode).toContain('const proxy = userProvidedProxy && userProvidedNamedHandler'); - // Should NOT wrap middleware (userProvidedMiddleware should remain false) - expect(wrappedCode).toContain('userProvidedMiddleware = false'); - expect(wrappedCode).toContain('const middleware = userProvidedMiddleware && userProvidedNamedHandler'); - - // Verify wrapMiddlewareWithSentry is called in the ternary - expect(wrappedCode).toContain('Sentry.wrapMiddlewareWithSentry(userProvidedNamedHandler)'); + // Proxy should be wrapped, middleware should be undefined + expect(wrappedCode).toMatch(/const proxy = userProvidedProxy \? wrappedHandler : undefined/); + expect(wrappedCode).toMatch(/const middleware = userProvidedMiddleware \? wrappedHandler : undefined/); }); - it('should only export "middleware" when user exports named "middleware" export', async () => { + it('should export middleware when user exports named "middleware" export', async () => { const callback = vi.fn(); const userCode = ` @@ -193,22 +189,18 @@ describe('wrappingLoader', () => { const wrappedCode = callback.mock.calls[0][1]; - // Verify both exports are present in the final export statement - expect(wrappedCode).toMatch(/export \{[^}]*\bmiddleware\b[^}]*\bproxy\b[^}]*\}/); - - // Should wrap middleware (userProvidedMiddleware should be true) + // Should detect middleware export expect(wrappedCode).toContain('userProvidedMiddleware = true'); - expect(wrappedCode).toContain('const middleware = userProvidedMiddleware && userProvidedNamedHandler'); - // Should NOT wrap proxy (userProvidedProxy should remain false) + // Should NOT detect proxy export expect(wrappedCode).toContain('userProvidedProxy = false'); - expect(wrappedCode).toContain('const proxy = userProvidedProxy && userProvidedNamedHandler'); - // Verify wrapMiddlewareWithSentry is called in the ternary - expect(wrappedCode).toContain('Sentry.wrapMiddlewareWithSentry(userProvidedNamedHandler)'); + // Middleware should be wrapped, proxy should be undefined + expect(wrappedCode).toMatch(/const middleware = userProvidedMiddleware \? wrappedHandler : undefined/); + expect(wrappedCode).toMatch(/const proxy = userProvidedProxy \? wrappedHandler : undefined/); }); - it('should not export named exports when user only exports default', async () => { + it('should export undefined middleware/proxy when user only exports default', async () => { const callback = vi.fn(); const userCode = ` @@ -252,9 +244,9 @@ describe('wrappingLoader', () => { expect(wrappedCode).toContain('userProvidedMiddleware = false'); expect(wrappedCode).toContain('userProvidedProxy = false'); - // Both named exports should evaluate to undefined (false && handler = undefined) - expect(wrappedCode).toMatch(/const middleware = userProvidedMiddleware && userProvidedNamedHandler/); - expect(wrappedCode).toMatch(/const proxy = userProvidedProxy && userProvidedNamedHandler/); + // Both middleware and proxy should be undefined (conditionals evaluate to false) + expect(wrappedCode).toMatch(/const middleware = userProvidedMiddleware \? wrappedHandler : undefined/); + expect(wrappedCode).toMatch(/const proxy = userProvidedProxy \? wrappedHandler : undefined/); }); }); });