Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 102 additions & 1 deletion packages/custom-client/src/__tests__/client.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, expect, it } from 'vitest';
import { describe, expect, it, vi } from 'vitest';

import { createClient } from '../client';

Expand Down Expand Up @@ -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);
});
});
25 changes: 24 additions & 1 deletion packages/custom-client/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion packages/custom-client/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ export const mergeHeaders = (

type ErrInterceptor<Err, Res, Req, Options> = (
error: Err,
response: Res,
response: Res | undefined,
request: Req,
options: Options,
) => Err | Promise<Err>;
Expand Down