diff --git a/packages/custom-client/src/__tests__/client.test.ts b/packages/custom-client/src/__tests__/client.test.ts index 4419039549..8eaab01344 100644 --- a/packages/custom-client/src/__tests__/client.test.ts +++ b/packages/custom-client/src/__tests__/client.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { createClient } from '../client'; @@ -48,3 +48,104 @@ describe('buildUrl', () => { expect(client.buildUrl(options)).toBe(url); }); }); + +describe('error interceptors', () => { + it('should call error interceptors when fetch throws network error', async () => { + const mockFetch = vi.fn().mockRejectedValue(new Error('Network error')); + const errorInterceptor = vi.fn().mockImplementation((error) => error); + + const client = createClient({ + baseUrl: 'https://example.com', + fetch: mockFetch, + }); + + client.interceptors.error.use(errorInterceptor); + + const result = await client.get({ url: '/test' }); + + expect(errorInterceptor).toHaveBeenCalledWith( + expect.any(Error), + undefined, // no response for network errors + expect.any(Request), + expect.any(Object), + ); + expect(result.error).toBeInstanceOf(Error); + expect(result.response).toBeUndefined(); + }); + + it('should call error interceptors when response is not ok', async () => { + const mockResponse = { + ok: false, + status: 404, + text: vi.fn().mockResolvedValue('Not found'), + } as unknown as Response; + + const mockFetch = vi.fn().mockResolvedValue(mockResponse); + const errorInterceptor = vi.fn().mockImplementation((error) => error); + + const client = createClient({ + baseUrl: 'https://example.com', + fetch: mockFetch, + }); + + client.interceptors.error.use(errorInterceptor); + + const result = await client.get({ url: '/test' }); + + expect(errorInterceptor).toHaveBeenCalledWith( + 'Not found', + mockResponse, + expect.any(Request), + expect.any(Object), + ); + expect(result.error).toBe('Not found'); + expect(result.response).toBe(mockResponse); + }); + + it('should throw error when throwOnError is true for network errors', async () => { + const networkError = new Error('Network error'); + const mockFetch = vi.fn().mockRejectedValue(networkError); + const errorInterceptor = vi.fn().mockImplementation((error) => error); + + const client = createClient({ + baseUrl: 'https://example.com', + fetch: mockFetch, + throwOnError: true, + }); + + client.interceptors.error.use(errorInterceptor); + + await expect(client.get({ url: '/test' })).rejects.toThrow('Network error'); + + expect(errorInterceptor).toHaveBeenCalledWith( + networkError, + undefined, + expect.any(Request), + expect.any(Object), + ); + }); + + it('should allow error interceptors to transform network errors', async () => { + const originalError = new Error('Network error'); + const transformedError = new Error('Transformed network error'); + const mockFetch = vi.fn().mockRejectedValue(originalError); + const errorInterceptor = vi.fn().mockReturnValue(transformedError); + + const client = createClient({ + baseUrl: 'https://example.com', + fetch: mockFetch, + }); + + client.interceptors.error.use(errorInterceptor); + + const result = await client.get({ url: '/test' }); + + expect(errorInterceptor).toHaveBeenCalledWith( + originalError, + undefined, + expect.any(Request), + expect.any(Object), + ); + expect(result.error).toBe(transformedError); + }); +}); diff --git a/packages/custom-client/src/client.ts b/packages/custom-client/src/client.ts index 2a5af631ac..61902850e2 100644 --- a/packages/custom-client/src/client.ts +++ b/packages/custom-client/src/client.ts @@ -77,7 +77,30 @@ export const createClient = (config: Config = {}): Client => { // fetch must be assigned here, otherwise it would throw the error: // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation const _fetch = opts.fetch!; - let response = await _fetch(request); + let response: Response; + + try { + response = await _fetch(request); + } catch (error) { + // Handle network-level errors (CORS, DNS, etc.) through error interceptors + let finalError = error; + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = (await fn(error, undefined, request, opts)) as unknown; + } + } + + if (opts.throwOnError) { + throw finalError; + } + + return { + error: finalError, + request, + response: undefined, + }; + } for (const fn of interceptors.response.fns) { if (fn) { diff --git a/packages/custom-client/src/utils.ts b/packages/custom-client/src/utils.ts index 7dcdf37653..17b8b45c9a 100644 --- a/packages/custom-client/src/utils.ts +++ b/packages/custom-client/src/utils.ts @@ -328,7 +328,7 @@ export const mergeHeaders = ( type ErrInterceptor = ( error: Err, - response: Res, + response: Res | undefined, request: Req, options: Options, ) => Err | Promise;