Skip to content

Commit 14bf0a0

Browse files
authored
fix(remix): Capture thrown fetch responses. (#10166)
Fixes: #10160 Potentially fixes: #10002 This PR adds support for extracting `header` data from thrown non-Remix responses (fetch responses) inside loaders / actions.
1 parent 9dbe87e commit 14bf0a0

File tree

6 files changed

+68
-11
lines changed

6 files changed

+68
-11
lines changed

packages/remix/src/utils/instrumentServer.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,12 @@ function isCatchResponse(response: Response): boolean {
6969
async function extractResponseError(response: Response): Promise<unknown> {
7070
const responseData = await extractData(response);
7171

72-
if (typeof responseData === 'string') {
73-
return responseData;
72+
if (typeof responseData === 'string' && responseData.length > 0) {
73+
return new Error(responseData);
7474
}
7575

7676
if (response.statusText) {
77-
return response.statusText;
77+
return new Error(response.statusText);
7878
}
7979

8080
return responseData;
@@ -92,7 +92,7 @@ export function wrapRemixHandleError(err: unknown, { request }: DataFunctionArgs
9292
// We are skipping thrown responses here as they are handled by
9393
// `captureRemixServerException` at loader / action level
9494
// We don't want to capture them twice.
95-
// This function if only for capturing unhandled server-side exceptions.
95+
// This function is only for capturing unhandled server-side exceptions.
9696
// https://remix.run/docs/en/main/file-conventions/entry.server#thrown-responses
9797
// https://remix.run/docs/en/v1/api/conventions#throwing-responses-in-loaders
9898
if (isResponse(err) || isRouteErrorResponse(err)) {
@@ -145,7 +145,7 @@ export async function captureRemixServerException(err: unknown, name: string, re
145145
captureException(isResponse(objectifiedErr) ? await extractResponseError(objectifiedErr) : objectifiedErr, scope => {
146146
// eslint-disable-next-line deprecation/deprecation
147147
const transaction = getActiveTransaction();
148-
const activeTransactionName = transaction ? spanToJSON(transaction) : undefined;
148+
const activeTransactionName = transaction ? spanToJSON(transaction).description : undefined;
149149

150150
scope.setSDKProcessingMetadata({
151151
request: {

packages/remix/src/utils/web-fetch.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable complexity */
12
// Based on Remix's implementation of Fetch API
23
// https://github.com/remix-run/web-std-io/blob/d2a003fe92096aaf97ab2a618b74875ccaadc280/packages/fetch/
34
// The MIT License (MIT)
@@ -70,12 +71,12 @@ export const getSearch = (parsedURL: URL): string => {
7071
export const normalizeRemixRequest = (request: RemixRequest): Record<string, any> => {
7172
const { requestInternalsSymbol, bodyInternalsSymbol } = getInternalSymbols(request);
7273

73-
if (!requestInternalsSymbol) {
74-
throw new Error('Could not find request internals symbol');
74+
if (!requestInternalsSymbol && !request.headers) {
75+
throw new Error('Could not find request headers');
7576
}
7677

77-
const { parsedURL } = request[requestInternalsSymbol];
78-
const headers = new Headers(request[requestInternalsSymbol].headers);
78+
const parsedURL = requestInternalsSymbol ? request[requestInternalsSymbol].parsedURL : new URL(request.url);
79+
const headers = requestInternalsSymbol ? new Headers(request[requestInternalsSymbol].headers) : request.headers;
7980

8081
// Fetch step 1.3
8182
if (!headers.has('Accept')) {
@@ -88,7 +89,7 @@ export const normalizeRemixRequest = (request: RemixRequest): Record<string, any
8889
contentLengthValue = '0';
8990
}
9091

91-
if (request.body !== null) {
92+
if (request.body !== null && request[bodyInternalsSymbol]) {
9293
const totalBytes = request[bodyInternalsSymbol].size;
9394
// Set Content-Length if totalBytes is a number (that is not NaN)
9495
if (typeof totalBytes === 'number' && !Number.isNaN(totalBytes)) {
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from '../../../common/routes/loader-throw-response.$id';
2+
export { default } from '../../../common/routes/loader-throw-response.$id';
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from '../../common/routes/loader-throw-response.$id';
2+
export { default } from '../../common/routes/loader-throw-response.$id';
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { LoaderFunction } from '@remix-run/node';
2+
import { useLoaderData } from '@remix-run/react';
3+
4+
export const loader: LoaderFunction = async ({ params: { id } }) => {
5+
if (id === '-1') {
6+
throw new Response(null, {
7+
status: 500,
8+
statusText: 'Not found',
9+
});
10+
}
11+
12+
return { message: 'hello world' };
13+
};
14+
15+
export default function LoaderThrowResponse() {
16+
const data = useLoaderData();
17+
18+
return (
19+
<div>
20+
<h1>Loader Throw Response</h1>
21+
<span>{data ? data.message : 'No Data'} </span>
22+
</div>
23+
);
24+
}

packages/remix/test/integration/test/server/loader.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,33 @@ describe.each(['builtin', 'express'])('Remix API Loaders with adapter = %s', ada
5050
});
5151
});
5252

53+
it('reports a thrown error response the loader', async () => {
54+
const env = await RemixTestEnv.init(adapter);
55+
const url = `${env.url}/loader-throw-response/-1`;
56+
57+
const envelopes = await env.getMultipleEnvelopeRequest({ url, count: 1, envelopeType: ['event'] });
58+
const event = envelopes[0][2];
59+
60+
assertSentryEvent(event, {
61+
exception: {
62+
values: [
63+
{
64+
type: 'Error',
65+
value: 'Not found',
66+
stacktrace: expect.any(Object),
67+
mechanism: {
68+
data: {
69+
function: 'loader',
70+
},
71+
handled: false,
72+
type: 'instrument',
73+
},
74+
},
75+
],
76+
},
77+
});
78+
});
79+
5380
it('correctly instruments a parameterized Remix API loader', async () => {
5481
const env = await RemixTestEnv.init(adapter);
5582
const url = `${env.url}/loader-json-response/123123`;
@@ -250,7 +277,8 @@ describe.each(['builtin', 'express'])('Remix API Loaders with adapter = %s', ada
250277

251278
const envelopesCount = await env.countEnvelopes({
252279
url,
253-
envelopeType: ['event'],
280+
envelopeType: 'event',
281+
timeout: 3000,
254282
});
255283

256284
expect(envelopesCount).toBe(0);

0 commit comments

Comments
 (0)