From aabf663e242717a336cccdfc5406367b0b2a4747 Mon Sep 17 00:00:00 2001 From: Oz Date: Sun, 13 Sep 2026 19:17:50 -0400 Subject: [PATCH] fix(start): don't log aborted server function requests as errors `serverFnFetcher` logged every non-`Response` error before rethrowing, including the `AbortError` produced when the caller cancels the request (e.g. TanStack Query aborting a query when its component unmounts). Cancellation is expected control flow, so it is no longer logged; genuine errors are still logged and all errors are still rethrown. Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/serverfn-dont-log-aborts.md | 7 ++++ .../src/client-rpc/serverFnFetcher.ts | 20 ++++++++++- .../tests/serverFnFetcher.test.ts | 33 +++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 .changeset/serverfn-dont-log-aborts.md create mode 100644 packages/start-client-core/tests/serverFnFetcher.test.ts diff --git a/.changeset/serverfn-dont-log-aborts.md b/.changeset/serverfn-dont-log-aborts.md new file mode 100644 index 00000000000..94de936f9fc --- /dev/null +++ b/.changeset/serverfn-dont-log-aborts.md @@ -0,0 +1,7 @@ +--- +'@tanstack/start-client-core': patch +--- + +fix(start): don't log an aborted server function request as an error + +`serverFnFetcher` logged every non-`Response` error before rethrowing, including the `AbortError` thrown when the caller cancels the request (e.g. TanStack Query aborting a query when its component unmounts). Cancellation is expected control flow, so it is no longer logged. Genuine errors are still logged, and all errors are still rethrown so callers can handle them. diff --git a/packages/start-client-core/src/client-rpc/serverFnFetcher.ts b/packages/start-client-core/src/client-rpc/serverFnFetcher.ts index 2534b6bb938..063f08d0915 100644 --- a/packages/start-client-core/src/client-rpc/serverFnFetcher.ts +++ b/packages/start-client-core/src/client-rpc/serverFnFetcher.ts @@ -222,6 +222,20 @@ async function getFetchBody( return undefined } +/** + * Whether an error is an abort, i.e. the caller cancelled the request (e.g. TanStack Query + * cancelling a query on unmount). Covers both `DOMException` (the standard `fetch` abort) and any + * `Error` with `name === 'AbortError'`. + */ +function isAbortError(error: unknown): boolean { + return ( + typeof error === 'object' && + error !== null && + 'name' in error && + (error as { name: unknown }).name === 'AbortError' + ) +} + /** * Retrieves a response from a given function and manages potential errors * and special response types including redirects and not found errors. @@ -238,7 +252,11 @@ async function getResponse(fn: () => Promise) { if (error instanceof Response) { response = error } else { - console.log(error) + // A caller aborting the request is expected control flow, not a failure, so don't log it. + // We still rethrow so the caller can handle the cancellation. + if (!isAbortError(error)) { + console.log(error) + } throw error } } diff --git a/packages/start-client-core/tests/serverFnFetcher.test.ts b/packages/start-client-core/tests/serverFnFetcher.test.ts new file mode 100644 index 00000000000..34f36749822 --- /dev/null +++ b/packages/start-client-core/tests/serverFnFetcher.test.ts @@ -0,0 +1,33 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { serverFnFetcher } from '../src/client-rpc/serverFnFetcher' + +describe('serverFnFetcher error handling', () => { + afterEach(() => { + vi.restoreAllMocks() + }) + + it('does not log when the request is aborted, but still rethrows', async () => { + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) + const abortError = new DOMException( + 'The operation was aborted', + 'AbortError', + ) + const handler = vi.fn((): Promise => Promise.reject(abortError)) + + await expect(serverFnFetcher('/_serverFn/test', [{}], handler)).rejects.toBe( + abortError, + ) + expect(logSpy).not.toHaveBeenCalled() + }) + + it('logs and rethrows a genuine error', async () => { + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) + const error = new Error('boom') + const handler = vi.fn((): Promise => Promise.reject(error)) + + await expect(serverFnFetcher('/_serverFn/test', [{}], handler)).rejects.toBe( + error, + ) + expect(logSpy).toHaveBeenCalledWith(error) + }) +})