diff --git a/packages/nextjs/src/config/templates/middlewareWrapperTemplate.ts b/packages/nextjs/src/config/templates/middlewareWrapperTemplate.ts index 236f4eff3999..30c77cf32952 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,14 @@ 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; +// 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 diff --git a/packages/nextjs/test/config/wrappingLoader.test.ts b/packages/nextjs/test/config/wrappingLoader.test.ts index 7f6e5f3a3c66..ab33450790bb 100644 --- a/packages/nextjs/test/config/wrappingLoader.test.ts +++ b/packages/nextjs/test/config/wrappingLoader.test.ts @@ -102,4 +102,151 @@ describe('wrappingLoader', () => { expect(callback).toHaveBeenCalledWith(null, expect.stringContaining("'/my/route'"), expect.anything()); }); + + describe('middleware wrapping', () => { + it('should 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 export statement (Rollup bundles this way) + expect(wrappedCode).toMatch(/export \{[^}]*\bmiddleware\b[^}]*\bproxy\b[^}]*\}/); + + // Should detect proxy export + expect(wrappedCode).toContain('userProvidedProxy = true'); + + // 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 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]; + + // Should detect middleware export + expect(wrappedCode).toContain('userProvidedMiddleware = true'); + + // Should NOT detect proxy export + expect(wrappedCode).toContain('userProvidedProxy = false'); + + // 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 export undefined middleware/proxy 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 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/); + }); + }); });