Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions packages/nextjs/src/config/templates/middlewareWrapperTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down
147 changes: 147 additions & 0 deletions packages/nextjs/test/config/wrappingLoader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>(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<WrappingLoaderOptions>;

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<void>(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<WrappingLoaderOptions>;

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<void>(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<WrappingLoaderOptions>;

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/);
});
});
});