Skip to content

Commit 50d2514

Browse files
authored
feat(node): Add fastify shouldHandleError (#15771)
Supercedes #13198 resolves #13197 Aligns fastify error handler with the express one. 1. Adds `shouldHandleError` to allow users to configure if errors should be captured 2. Makes sure the default `shouldHandleError` does not capture errors for 4xx and 3xx status codes. ## Usage ```js setupFastifyErrorHandler(app, { shouldHandleError(_error, _request, reply) { return statusCode >= 500 || statusCode <= 399; }, }); ```
1 parent ba5993c commit 50d2514

File tree

3 files changed

+118
-17
lines changed

3 files changed

+118
-17
lines changed

dev-packages/e2e-tests/test-applications/node-fastify/src/app.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,16 @@ app.get('/test-error', async function (req, res) {
8181
res.send({ exceptionId });
8282
});
8383

84+
app.get('/test-4xx-error', async function (req, res) {
85+
res.code(400);
86+
throw new Error('This is a 4xx error');
87+
});
88+
89+
app.get('/test-5xx-error', async function (req, res) {
90+
res.code(500);
91+
throw new Error('This is a 5xx error');
92+
});
93+
8494
app.get<{ Params: { id: string } }>('/test-exception/:id', async function (req, res) {
8595
throw new Error(`This is an exception with id ${req.params.id}`);
8696
});

dev-packages/e2e-tests/test-applications/node-fastify/tests/errors.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,25 @@ test('Sends correct error event', async ({ baseURL }) => {
2828
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
2929
});
3030
});
31+
32+
test('Does not send 4xx errors by default', async ({ baseURL }) => {
33+
// Define our test approach: we'll send both a 5xx and a 4xx request
34+
// We should only see the 5xx error captured due to shouldHandleError's default behavior
35+
36+
// Create a promise to wait for the 500 error
37+
const serverErrorPromise = waitForError('node-fastify', event => {
38+
// Looking for a 500 error that should be captured
39+
return !!event.exception?.values?.[0]?.value?.includes('This is a 5xx error');
40+
});
41+
42+
// Make a request that will trigger a 400 error
43+
const notFoundResponse = await fetch(`${baseURL}/test-4xx-error`);
44+
expect(notFoundResponse.status).toBe(400);
45+
46+
// Make a request that will trigger a 500 error
47+
await fetch(`${baseURL}/test-5xx-error`);
48+
49+
// Verify we receive the 500 error
50+
const errorEvent = await serverErrorPromise;
51+
expect(errorEvent.exception?.values?.[0]?.value).toContain('This is a 5xx error');
52+
});

packages/node/src/integrations/tracing/fastify.ts

Lines changed: 86 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,14 @@ import type { IntegrationFn, Span } from '@sentry/core';
1212
import { generateInstrumentOnce } from '../../otel/instrument';
1313
import { ensureIsWrapped } from '../../utils/ensureIsWrapped';
1414

15-
// We inline the types we care about here
16-
interface Fastify {
17-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
18-
register: (plugin: any) => void;
19-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
20-
addHook: (hook: string, handler: (request: any, reply: any, error: Error) => void) => void;
21-
}
22-
2315
/**
2416
* Minimal request type containing properties around route information.
2517
* Works for Fastify 3, 4 and presumably 5.
18+
*
19+
* Based on https://github.com/fastify/fastify/blob/ce3811f5f718be278bbcd4392c615d64230065a6/types/request.d.ts
2620
*/
27-
interface FastifyRequestRouteInfo {
21+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
22+
interface MinimalFastifyRequest extends Record<string, any> {
2823
method?: string;
2924
// since fastify@4.10.0
3025
routeOptions?: {
@@ -33,6 +28,66 @@ interface FastifyRequestRouteInfo {
3328
routerPath?: string;
3429
}
3530

31+
/**
32+
* Minimal reply type containing properties needed for error handling.
33+
*
34+
* Based on https://github.com/fastify/fastify/blob/ce3811f5f718be278bbcd4392c615d64230065a6/types/reply.d.ts
35+
*/
36+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
37+
interface MinimalFastifyReply extends Record<string, any> {
38+
statusCode: number;
39+
}
40+
41+
// We inline the types we care about here
42+
interface Fastify {
43+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
44+
register: (plugin: any) => void;
45+
addHook: (hook: string, handler: (...params: unknown[]) => void) => void;
46+
}
47+
48+
interface FastifyWithHooks extends Omit<Fastify, 'addHook'> {
49+
addHook(
50+
hook: 'onError',
51+
handler: (request: MinimalFastifyRequest, reply: MinimalFastifyReply, error: Error) => void,
52+
): void;
53+
addHook(hook: 'onRequest', handler: (request: MinimalFastifyRequest, reply: MinimalFastifyReply) => void): void;
54+
}
55+
56+
interface FastifyHandlerOptions {
57+
/**
58+
* Callback method deciding whether error should be captured and sent to Sentry
59+
*
60+
* @param error Captured Fastify error
61+
* @param request Fastify request (or any object containing at least method, routeOptions.url, and routerPath)
62+
* @param reply Fastify reply (or any object containing at least statusCode)
63+
*
64+
* @example
65+
*
66+
* ```javascript
67+
* setupFastifyErrorHandler(app, {
68+
* shouldHandleError(_error, _request, reply) {
69+
* return reply.statusCode >= 400;
70+
* },
71+
* });
72+
* ```
73+
*
74+
* If using TypeScript, you can cast the request and reply to get full type safety.
75+
*
76+
* ```typescript
77+
* import type { FastifyRequest, FastifyReply } from 'fastify';
78+
*
79+
* setupFastifyErrorHandler(app, {
80+
* shouldHandleError(error, minimalRequest, minimalReply) {
81+
* const request = minimalRequest as FastifyRequest;
82+
* const reply = minimalReply as FastifyReply;
83+
* return reply.statusCode >= 500;
84+
* },
85+
* });
86+
* ```
87+
*/
88+
shouldHandleError: (error: Error, request: MinimalFastifyRequest, reply: MinimalFastifyReply) => boolean;
89+
}
90+
3691
const INTEGRATION_NAME = 'Fastify';
3792

3893
export const instrumentFastify = generateInstrumentOnce(
@@ -73,10 +128,22 @@ const _fastifyIntegration = (() => {
73128
*/
74129
export const fastifyIntegration = defineIntegration(_fastifyIntegration);
75130

131+
/**
132+
* Default function to determine if an error should be sent to Sentry
133+
*
134+
* 3xx and 4xx errors are not sent by default.
135+
*/
136+
function defaultShouldHandleError(_error: Error, _request: MinimalFastifyRequest, reply: MinimalFastifyReply): boolean {
137+
const statusCode = reply.statusCode;
138+
// 3xx and 4xx errors are not sent by default.
139+
return statusCode >= 500 || statusCode <= 299;
140+
}
141+
76142
/**
77143
* Add an Fastify error handler to capture errors to Sentry.
78144
*
79145
* @param fastify The Fastify instance to which to add the error handler
146+
* @param options Configuration options for the handler
80147
*
81148
* @example
82149
* ```javascript
@@ -92,23 +159,25 @@ export const fastifyIntegration = defineIntegration(_fastifyIntegration);
92159
* app.listen({ port: 3000 });
93160
* ```
94161
*/
95-
export function setupFastifyErrorHandler(fastify: Fastify): void {
162+
export function setupFastifyErrorHandler(fastify: Fastify, options?: Partial<FastifyHandlerOptions>): void {
163+
const shouldHandleError = options?.shouldHandleError || defaultShouldHandleError;
164+
96165
const plugin = Object.assign(
97-
function (fastify: Fastify, _options: unknown, done: () => void): void {
98-
fastify.addHook('onError', async (_request, _reply, error) => {
99-
captureException(error);
166+
function (fastify: FastifyWithHooks, _options: unknown, done: () => void): void {
167+
fastify.addHook('onError', async (request, reply, error) => {
168+
if (shouldHandleError(error, request, reply)) {
169+
captureException(error);
170+
}
100171
});
101172

102173
// registering `onRequest` hook here instead of using Otel `onRequest` callback b/c `onRequest` hook
103174
// is ironically called in the fastify `preHandler` hook which is called later in the lifecycle:
104175
// https://fastify.dev/docs/latest/Reference/Lifecycle/
105176
fastify.addHook('onRequest', async (request, _reply) => {
106-
const reqWithRouteInfo = request as FastifyRequestRouteInfo;
107-
108177
// Taken from Otel Fastify instrumentation:
109178
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-fastify/src/instrumentation.ts#L94-L96
110-
const routeName = reqWithRouteInfo.routeOptions?.url || reqWithRouteInfo.routerPath;
111-
const method = reqWithRouteInfo.method || 'GET';
179+
const routeName = request.routeOptions?.url || request.routerPath;
180+
const method = request.method || 'GET';
112181

113182
getIsolationScope().setTransactionName(`${method} ${routeName}`);
114183
});

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy