fix(utils/ofetch): surface root causes in fetch errors (#22718)

* fix(ofetch): enhance error handling to surface root causes in fetch errors

* fix(errors): remove duplicated error name on dev
This commit is contained in:
Tony 2026-07-14 21:52:50 +08:00 committed by GitHub
parent 2266ac7ec8
commit 097a56187f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 17 additions and 5 deletions

View File

@ -53,7 +53,7 @@ export const errorHandler: ErrorHandler = (error, ctx) => {
});
}
let errorMessage = (process.env.NODE_ENV || process.env.VERCEL_ENV) === 'production' ? error.message : error.stack || error.message;
let errorMessage = (process.env.NODE_ENV || process.env.VERCEL_ENV) === 'production' || !error.stack ? `${error.name}: ${error.message}` : error.stack;
switch (error.name) {
case 'HTTPError':
case 'RequestError':
@ -75,9 +75,7 @@ export const errorHandler: ErrorHandler = (error, ctx) => {
ctx.status(503);
break;
}
const message = `${error.name}: ${errorMessage}`;
logger.error(`Error in ${requestPath}: ${message}`);
logger.error(`Error in ${requestPath}: ${errorMessage}`);
requestMetric.error({ path: matchedRoute, method: ctx.req.method, status: ctx.res.status });
return config.isPackage || ctx.req.query('format') === 'json'
@ -86,7 +84,7 @@ export const errorHandler: ErrorHandler = (error, ctx) => {
message: error.message ?? error,
},
})
: ctx.html(<Error requestPath={requestPath} message={message} errorRoute={hasMatchedRoute ? matchedRoute : requestPath} nodeVersion={process.version} />);
: ctx.html(<Error requestPath={requestPath} message={errorMessage} errorRoute={hasMatchedRoute ? matchedRoute : requestPath} nodeVersion={process.version} />);
};
export const notFoundHandler: NotFoundHandler = (ctx) => errorHandler(new NotFoundError(), ctx);

View File

@ -36,6 +36,15 @@ describe('ofetch', () => {
expect(warnSpy).toHaveBeenCalled();
});
it('surfaces the root cause in fetch error messages', async () => {
const { logger, ofetch } = await loadOfetchWithLogger();
vi.spyOn(logger, 'error').mockImplementation(() => logger);
const networkError = new TypeError('fetch failed', { cause: new Error('getaddrinfo ENOTFOUND t.me') });
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(networkError));
await expect(ofetch('https://t.me/s/telegram', { retry: 0 })).rejects.toThrow('fetch failed (getaddrinfo ENOTFOUND t.me)');
});
it('logs redirected responses', async () => {
const { logger, ofetch } = await loadOfetchWithLogger();
const httpSpy = vi.spyOn(logger, 'http').mockImplementation(() => logger);

View File

@ -35,6 +35,11 @@ const rofetch = createFetch({ fetch: (input: Parameters<typeof fetch>[0], init?:
},
onRequestError({ request, error }) {
logger.error(`Request ${request} fail: ${error.cause} ${error}`);
for (let cause: unknown = error.cause; cause instanceof Error; cause = cause.cause) {
if (cause.message) {
error.message += ` (${cause.message})`;
}
}
},
onResponse({ request, response }) {
if (response.redirected) {