Skip to content

Commit 48f1a4f

Browse files
authored
fix(cloudflare): Capture exceptions thrown in hono (#16355)
Our cloudflare SDK captures fetch exceptions in the catch block. But hono never reaches [this block](https://github.com/getsentry/sentry-javascript/blob/a874d763171289b5556973a4842f580d4bbb7ec0/packages/cloudflare/src/request.ts#L85-L90) where we send exceptions. hono processes errors with their `onError` function (or `errorHandler`) and we need to capture the exception there. This PR wraps the [`errorHandler` of hono](https://github.com/honojs/hono/blob/bb7afaccfd5b6b514da356e069f27eb5ccfc0e3b/src/hono-base.ts#L36 ). Will create an E2E test in another PR.
1 parent b4bd21f commit 48f1a4f

File tree

2 files changed

+112
-0
lines changed

2 files changed

+112
-0
lines changed

packages/cloudflare/src/handler.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { init } from './sdk';
2626
* @param handler {ExportedHandler} The handler to wrap.
2727
* @returns The wrapped handler.
2828
*/
29+
// eslint-disable-next-line complexity
2930
export function withSentry<Env = unknown, QueueHandlerMessage = unknown, CfHostMetadata = unknown>(
3031
optionsCallback: (env: Env) => CloudflareOptions,
3132
handler: ExportedHandler<Env, QueueHandlerMessage, CfHostMetadata>,
@@ -47,6 +48,26 @@ export function withSentry<Env = unknown, QueueHandlerMessage = unknown, CfHostM
4748
markAsInstrumented(handler.fetch);
4849
}
4950

51+
/* hono does not reach the catch block of the fetch handler and captureException needs to be called in the hono errorHandler */
52+
if (
53+
'onError' in handler &&
54+
'errorHandler' in handler &&
55+
typeof handler.errorHandler === 'function' &&
56+
!isInstrumented(handler.errorHandler)
57+
) {
58+
handler.errorHandler = new Proxy(handler.errorHandler, {
59+
apply(target, thisArg, args) {
60+
const [err] = args;
61+
62+
captureException(err, { mechanism: { handled: false, type: 'cloudflare' } });
63+
64+
return Reflect.apply(target, thisArg, args);
65+
},
66+
});
67+
68+
markAsInstrumented(handler.errorHandler);
69+
}
70+
5071
if ('scheduled' in handler && typeof handler.scheduled === 'function' && !isInstrumented(handler.scheduled)) {
5172
handler.scheduled = new Proxy(handler.scheduled, {
5273
apply(target, thisArg, args: Parameters<ExportedHandlerScheduledHandler<Env>>) {

packages/cloudflare/test/handler.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ import * as SentryCore from '@sentry/core';
77
import { beforeEach, describe, expect, test, vi } from 'vitest';
88
import { CloudflareClient } from '../src/client';
99
import { withSentry } from '../src/handler';
10+
import { markAsInstrumented } from '../src/instrument';
11+
12+
// Custom type for hono-like apps (cloudflare handlers) that include errorHandler and onError
13+
type HonoLikeApp<Env = unknown, QueueHandlerMessage = unknown, CfHostMetadata = unknown> = ExportedHandler<
14+
Env,
15+
QueueHandlerMessage,
16+
CfHostMetadata
17+
> & {
18+
onError?: () => void;
19+
errorHandler?: (err: Error) => Response;
20+
};
1021

1122
const MOCK_ENV = {
1223
SENTRY_DSN: 'https://[email protected]/1337',
@@ -931,6 +942,86 @@ describe('withSentry', () => {
931942
});
932943
});
933944
});
945+
946+
describe('hono errorHandler', () => {
947+
test('captures errors handled by the errorHandler', async () => {
948+
const captureExceptionSpy = vi.spyOn(SentryCore, 'captureException');
949+
const error = new Error('test hono error');
950+
951+
const honoApp = {
952+
fetch(_request, _env, _context) {
953+
return new Response('test');
954+
},
955+
onError() {}, // hono-like onError
956+
errorHandler(err: Error) {
957+
return new Response(`Error: ${err.message}`, { status: 500 });
958+
},
959+
} satisfies HonoLikeApp<typeof MOCK_ENV>;
960+
961+
withSentry(env => ({ dsn: env.SENTRY_DSN }), honoApp);
962+
963+
// simulates hono's error handling
964+
const errorHandlerResponse = honoApp.errorHandler?.(error);
965+
966+
expect(captureExceptionSpy).toHaveBeenCalledTimes(1);
967+
expect(captureExceptionSpy).toHaveBeenLastCalledWith(error, {
968+
mechanism: { handled: false, type: 'cloudflare' },
969+
});
970+
expect(errorHandlerResponse?.status).toBe(500);
971+
});
972+
973+
test('preserves the original errorHandler functionality', async () => {
974+
const originalErrorHandlerSpy = vi.fn().mockImplementation((err: Error) => {
975+
return new Response(`Error: ${err.message}`, { status: 500 });
976+
});
977+
978+
const error = new Error('test hono error');
979+
980+
const honoApp = {
981+
fetch(_request, _env, _context) {
982+
return new Response('test');
983+
},
984+
onError() {}, // hono-like onError
985+
errorHandler: originalErrorHandlerSpy,
986+
} satisfies HonoLikeApp<typeof MOCK_ENV>;
987+
988+
withSentry(env => ({ dsn: env.SENTRY_DSN }), honoApp);
989+
990+
// Call the errorHandler directly to simulate Hono's error handling
991+
const errorHandlerResponse = honoApp.errorHandler?.(error);
992+
993+
expect(originalErrorHandlerSpy).toHaveBeenCalledTimes(1);
994+
expect(originalErrorHandlerSpy).toHaveBeenLastCalledWith(error);
995+
expect(errorHandlerResponse?.status).toBe(500);
996+
});
997+
998+
test('does not instrument an already instrumented errorHandler', async () => {
999+
const captureExceptionSpy = vi.spyOn(SentryCore, 'captureException');
1000+
const error = new Error('test hono error');
1001+
1002+
// Create a handler with an errorHandler that's already been instrumented
1003+
const originalErrorHandler = (err: Error) => {
1004+
return new Response(`Error: ${err.message}`, { status: 500 });
1005+
};
1006+
1007+
// Mark as instrumented before wrapping
1008+
markAsInstrumented(originalErrorHandler);
1009+
1010+
const honoApp = {
1011+
fetch(_request, _env, _context) {
1012+
return new Response('test');
1013+
},
1014+
onError() {}, // hono-like onError
1015+
errorHandler: originalErrorHandler,
1016+
} satisfies HonoLikeApp<typeof MOCK_ENV>;
1017+
1018+
withSentry(env => ({ dsn: env.SENTRY_DSN }), honoApp);
1019+
1020+
// The errorHandler should not have been wrapped again
1021+
honoApp.errorHandler?.(error);
1022+
expect(captureExceptionSpy).not.toHaveBeenCalled();
1023+
});
1024+
});
9341025
});
9351026

9361027
function createMockExecutionContext(): ExecutionContext {

0 commit comments

Comments
 (0)