Skip to content

Commit 33d5898

Browse files
authored
fix(nextjs): Update proxy template wrapping (#18086)
This fixes an issue when we are wrapping Next.js 16+ proxy.ts files, the webpack loader was exporting both middleware and proxy named exports, causing Next.js to potentially treat proxy files incorrectly as middleware files. closes #18001
1 parent 600e27a commit 33d5898

File tree

2 files changed

+159
-4
lines changed

2 files changed

+159
-4
lines changed

packages/nextjs/src/config/templates/middlewareWrapperTemplate.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,17 @@ const userApiModule = origModule as NextApiModule;
2626
// the case Next.js wil crash during runtime but the Sentry SDK should definitely not crash so we need tohandle it.
2727
let userProvidedNamedHandler: EdgeRouteHandler | undefined = undefined;
2828
let userProvidedDefaultHandler: EdgeRouteHandler | undefined = undefined;
29+
let userProvidedMiddleware = false;
30+
let userProvidedProxy = false;
2931

3032
if ('middleware' in userApiModule && typeof userApiModule.middleware === 'function') {
3133
// Handle when user defines via named ESM export: `export { middleware };`
3234
userProvidedNamedHandler = userApiModule.middleware;
35+
userProvidedMiddleware = true;
3336
} else if ('proxy' in userApiModule && typeof userApiModule.proxy === 'function') {
3437
// Handle when user defines via named ESM export (Next.js 16): `export { proxy };`
3538
userProvidedNamedHandler = userApiModule.proxy;
39+
userProvidedProxy = true;
3640
} else if ('default' in userApiModule && typeof userApiModule.default === 'function') {
3741
// Handle when user defines via ESM export: `export default myFunction;`
3842
userProvidedDefaultHandler = userApiModule.default;
@@ -41,10 +45,14 @@ if ('middleware' in userApiModule && typeof userApiModule.middleware === 'functi
4145
userProvidedDefaultHandler = userApiModule;
4246
}
4347

44-
export const middleware = userProvidedNamedHandler
45-
? Sentry.wrapMiddlewareWithSentry(userProvidedNamedHandler)
46-
: undefined;
47-
export const proxy = userProvidedNamedHandler ? Sentry.wrapMiddlewareWithSentry(userProvidedNamedHandler) : undefined;
48+
// Wrap the handler that the user provided (middleware, proxy, or default)
49+
// We preserve the original export names so Next.js can handle its internal renaming logic
50+
const wrappedHandler = userProvidedNamedHandler ? Sentry.wrapMiddlewareWithSentry(userProvidedNamedHandler) : undefined;
51+
52+
// Only export the named export that the user actually provided
53+
// This ensures Next.js sees the same export structure and can apply its renaming logic
54+
export const middleware = userProvidedMiddleware ? wrappedHandler : undefined;
55+
export const proxy = userProvidedProxy ? wrappedHandler : undefined;
4856
export default userProvidedDefaultHandler ? Sentry.wrapMiddlewareWithSentry(userProvidedDefaultHandler) : undefined;
4957

5058
// Re-export anything exported by the page module we're wrapping. When processing this code, Rollup is smart enough to

packages/nextjs/test/config/wrappingLoader.test.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,151 @@ describe('wrappingLoader', () => {
102102

103103
expect(callback).toHaveBeenCalledWith(null, expect.stringContaining("'/my/route'"), expect.anything());
104104
});
105+
106+
describe('middleware wrapping', () => {
107+
it('should export proxy when user exports named "proxy" export', async () => {
108+
const callback = vi.fn();
109+
110+
const userCode = `
111+
export function proxy(request) {
112+
return new Response('ok');
113+
}
114+
`;
115+
const userCodeSourceMap = undefined;
116+
117+
const loaderPromise = new Promise<void>(resolve => {
118+
const loaderThis = {
119+
...defaultLoaderThis,
120+
resourcePath: '/my/src/proxy.ts',
121+
callback: callback.mockImplementation(() => {
122+
resolve();
123+
}),
124+
getOptions() {
125+
return {
126+
pagesDir: '/my/pages',
127+
appDir: '/my/app',
128+
pageExtensionRegex: DEFAULT_PAGE_EXTENSION_REGEX,
129+
excludeServerRoutes: [],
130+
wrappingTargetKind: 'middleware',
131+
vercelCronsConfig: undefined,
132+
nextjsRequestAsyncStorageModulePath: '/my/request-async-storage.js',
133+
};
134+
},
135+
} satisfies LoaderThis<WrappingLoaderOptions>;
136+
137+
wrappingLoader.call(loaderThis, userCode, userCodeSourceMap);
138+
});
139+
140+
await loaderPromise;
141+
142+
const wrappedCode = callback.mock.calls[0][1];
143+
144+
// Verify both exports are present in export statement (Rollup bundles this way)
145+
expect(wrappedCode).toMatch(/export \{[^}]*\bmiddleware\b[^}]*\bproxy\b[^}]*\}/);
146+
147+
// Should detect proxy export
148+
expect(wrappedCode).toContain('userProvidedProxy = true');
149+
150+
// Proxy should be wrapped, middleware should be undefined
151+
expect(wrappedCode).toMatch(/const proxy = userProvidedProxy \? wrappedHandler : undefined/);
152+
expect(wrappedCode).toMatch(/const middleware = userProvidedMiddleware \? wrappedHandler : undefined/);
153+
});
154+
155+
it('should export middleware when user exports named "middleware" export', async () => {
156+
const callback = vi.fn();
157+
158+
const userCode = `
159+
export function middleware(request) {
160+
return new Response('ok');
161+
}
162+
`;
163+
const userCodeSourceMap = undefined;
164+
165+
const loaderPromise = new Promise<void>(resolve => {
166+
const loaderThis = {
167+
...defaultLoaderThis,
168+
resourcePath: '/my/src/middleware.ts',
169+
callback: callback.mockImplementation(() => {
170+
resolve();
171+
}),
172+
getOptions() {
173+
return {
174+
pagesDir: '/my/pages',
175+
appDir: '/my/app',
176+
pageExtensionRegex: DEFAULT_PAGE_EXTENSION_REGEX,
177+
excludeServerRoutes: [],
178+
wrappingTargetKind: 'middleware',
179+
vercelCronsConfig: undefined,
180+
nextjsRequestAsyncStorageModulePath: '/my/request-async-storage.js',
181+
};
182+
},
183+
} satisfies LoaderThis<WrappingLoaderOptions>;
184+
185+
wrappingLoader.call(loaderThis, userCode, userCodeSourceMap);
186+
});
187+
188+
await loaderPromise;
189+
190+
const wrappedCode = callback.mock.calls[0][1];
191+
192+
// Should detect middleware export
193+
expect(wrappedCode).toContain('userProvidedMiddleware = true');
194+
195+
// Should NOT detect proxy export
196+
expect(wrappedCode).toContain('userProvidedProxy = false');
197+
198+
// Middleware should be wrapped, proxy should be undefined
199+
expect(wrappedCode).toMatch(/const middleware = userProvidedMiddleware \? wrappedHandler : undefined/);
200+
expect(wrappedCode).toMatch(/const proxy = userProvidedProxy \? wrappedHandler : undefined/);
201+
});
202+
203+
it('should export undefined middleware/proxy when user only exports default', async () => {
204+
const callback = vi.fn();
205+
206+
const userCode = `
207+
export default function(request) {
208+
return new Response('ok');
209+
}
210+
`;
211+
const userCodeSourceMap = undefined;
212+
213+
const loaderPromise = new Promise<void>(resolve => {
214+
const loaderThis = {
215+
...defaultLoaderThis,
216+
resourcePath: '/my/src/middleware.ts',
217+
callback: callback.mockImplementation(() => {
218+
resolve();
219+
}),
220+
getOptions() {
221+
return {
222+
pagesDir: '/my/pages',
223+
appDir: '/my/app',
224+
pageExtensionRegex: DEFAULT_PAGE_EXTENSION_REGEX,
225+
excludeServerRoutes: [],
226+
wrappingTargetKind: 'middleware',
227+
vercelCronsConfig: undefined,
228+
nextjsRequestAsyncStorageModulePath: '/my/request-async-storage.js',
229+
};
230+
},
231+
} satisfies LoaderThis<WrappingLoaderOptions>;
232+
233+
wrappingLoader.call(loaderThis, userCode, userCodeSourceMap);
234+
});
235+
236+
await loaderPromise;
237+
238+
const wrappedCode = callback.mock.calls[0][1];
239+
240+
// Should export default
241+
expect(wrappedCode).toMatch(/export \{[^}]* as default[^}]*\}/);
242+
243+
// Both flags should be false (no named exports provided by user)
244+
expect(wrappedCode).toContain('userProvidedMiddleware = false');
245+
expect(wrappedCode).toContain('userProvidedProxy = false');
246+
247+
// Both middleware and proxy should be undefined (conditionals evaluate to false)
248+
expect(wrappedCode).toMatch(/const middleware = userProvidedMiddleware \? wrappedHandler : undefined/);
249+
expect(wrappedCode).toMatch(/const proxy = userProvidedProxy \? wrappedHandler : undefined/);
250+
});
251+
});
105252
});

0 commit comments

Comments
 (0)