Skip to content

Commit c94138a

Browse files
committed
Handle 404 responses in RSC server function wrapper
1 parent 8745f1c commit c94138a

2 files changed

Lines changed: 29 additions & 2 deletions

File tree

packages/react-router/src/server/rsc/wrapServerFunction.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@ import {
1010
SPAN_STATUS_OK,
1111
startSpan,
1212
} from '@sentry/core';
13-
import { isErrorCaptured, isRedirectResponse, markErrorAsCaptured, safeFlushServerless } from './responseUtils';
13+
import {
14+
isErrorCaptured,
15+
isNotFoundResponse,
16+
isRedirectResponse,
17+
markErrorAsCaptured,
18+
safeFlushServerless,
19+
} from './responseUtils';
1420
import type { WrapServerFunctionOptions } from './types';
1521

1622
/**
@@ -73,11 +79,16 @@ export function wrapServerFunction<T extends (...args: any[]) => Promise<any>>(
7379
} catch (error) {
7480
// Check if the error is a redirect (common pattern in server functions)
7581
if (isRedirectResponse(error)) {
76-
// Don't capture redirects as errors, but still end the span
7782
span.setStatus({ code: SPAN_STATUS_OK });
7883
throw error;
7984
}
8085

86+
// Check if the error is a not-found response (404)
87+
if (isNotFoundResponse(error)) {
88+
span.setStatus({ code: SPAN_STATUS_ERROR, message: 'not_found' });
89+
throw error;
90+
}
91+
8192
span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' });
8293

8394
// Only capture if not already captured (error may bubble through nested server functions or components)

packages/react-router/test/server/rsc/wrapServerFunction.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,22 @@ describe('wrapServerFunction', () => {
138138
expect(core.captureException).not.toHaveBeenCalled();
139139
});
140140

141+
it('should not capture not-found errors as exceptions', async () => {
142+
const notFoundResponse = new Response(null, { status: 404 });
143+
const mockServerFn = vi.fn().mockRejectedValue(notFoundResponse);
144+
const mockSetStatus = vi.fn();
145+
const mockSetTransactionName = vi.fn();
146+
147+
(core.getIsolationScope as any).mockReturnValue({ setTransactionName: mockSetTransactionName });
148+
(core.startSpan as any).mockImplementation((_: any, fn: any) => fn({ setStatus: mockSetStatus }));
149+
150+
const wrappedFn = wrapServerFunction('testFunction', mockServerFn);
151+
152+
await expect(wrappedFn()).rejects.toBe(notFoundResponse);
153+
expect(mockSetStatus).toHaveBeenCalledWith({ code: 2, message: 'not_found' });
154+
expect(core.captureException).not.toHaveBeenCalled();
155+
});
156+
141157
it('should preserve function name', () => {
142158
const mockServerFn = vi.fn().mockResolvedValue('result');
143159
const wrappedFn = wrapServerFunction('testFunction', mockServerFn);

0 commit comments

Comments
 (0)