From 16abc46ffd16ba6bbd2c75a8a8d93c7864d20f84 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Sep 2025 11:22:10 +0000 Subject: [PATCH 1/2] Initial plan From 1776d8f982c01fe3e36d59600883f3fd8cdd3973 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Sep 2025 11:42:56 +0000 Subject: [PATCH 2/2] Fix error interceptors for network failures - Wrap fetch call in try-catch to handle network-level errors (CORS, DNS, etc.) - Update error interceptor type signature to allow undefined response - Add comprehensive tests for both network and response errors - Maintain backward compatibility for existing error handling Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com> --- .../src/__tests__/client.test.ts | 103 +++++++++++++++++- packages/custom-client/src/client.ts | 25 ++++- packages/custom-client/src/utils.ts | 2 +- 3 files changed, 127 insertions(+), 3 deletions(-) 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;