fork of hey-api/openapi-ts because I need some additional things

Fix fetch client to intercept AbortError and network exceptions

Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com>

+139 -1
+105
packages/openapi-ts/src/plugins/@hey-api/client-fetch/__tests__/client.test.ts
··· 406 406 }, 407 407 ); 408 408 }); 409 + 410 + describe('error interceptor for fetch exceptions', () => { 411 + it('intercepts AbortError when fetch is aborted', async () => { 412 + const client = createClient({ baseUrl: 'https://example.com' }); 413 + 414 + const abortError = new DOMException( 415 + 'The operation was aborted', 416 + 'AbortError', 417 + ); 418 + const mockFetch: MockFetch = vi.fn().mockRejectedValue(abortError); 419 + 420 + const mockErrorInterceptor = vi.fn().mockImplementation((error) => { 421 + expect(error).toBe(abortError); 422 + return { message: 'Request was aborted', type: 'abort' }; 423 + }); 424 + 425 + const interceptorId = client.interceptors.error.use(mockErrorInterceptor); 426 + 427 + const result = await client.get({ 428 + fetch: mockFetch, 429 + url: '/test', 430 + }); 431 + 432 + expect(mockErrorInterceptor).toHaveBeenCalledOnce(); 433 + expect(result.error).toEqual({ 434 + message: 'Request was aborted', 435 + type: 'abort', 436 + }); 437 + 438 + client.interceptors.error.eject(interceptorId); 439 + }); 440 + 441 + it('intercepts network errors', async () => { 442 + const client = createClient({ baseUrl: 'https://example.com' }); 443 + 444 + const networkError = new TypeError('Failed to fetch'); 445 + const mockFetch: MockFetch = vi.fn().mockRejectedValue(networkError); 446 + 447 + const mockErrorInterceptor = vi.fn().mockImplementation((error) => { 448 + expect(error).toBe(networkError); 449 + return { message: 'Network error occurred', type: 'network' }; 450 + }); 451 + 452 + const interceptorId = client.interceptors.error.use(mockErrorInterceptor); 453 + 454 + const result = await client.get({ 455 + fetch: mockFetch, 456 + url: '/test', 457 + }); 458 + 459 + expect(mockErrorInterceptor).toHaveBeenCalledOnce(); 460 + expect(result.error).toEqual({ 461 + message: 'Network error occurred', 462 + type: 'network', 463 + }); 464 + 465 + client.interceptors.error.eject(interceptorId); 466 + }); 467 + 468 + it('throws AbortError when throwOnError is true', async () => { 469 + const client = createClient({ baseUrl: 'https://example.com' }); 470 + 471 + const abortError = new DOMException( 472 + 'The operation was aborted', 473 + 'AbortError', 474 + ); 475 + const mockFetch: MockFetch = vi.fn().mockRejectedValue(abortError); 476 + 477 + const mockErrorInterceptor = vi.fn().mockImplementation(() => ({ 478 + message: 'Request was aborted', 479 + type: 'abort', 480 + })); 481 + 482 + const interceptorId = client.interceptors.error.use(mockErrorInterceptor); 483 + 484 + await expect( 485 + client.get({ 486 + fetch: mockFetch, 487 + throwOnError: true, 488 + url: '/test', 489 + }), 490 + ).rejects.toEqual({ message: 'Request was aborted', type: 'abort' }); 491 + 492 + expect(mockErrorInterceptor).toHaveBeenCalledOnce(); 493 + 494 + client.interceptors.error.eject(interceptorId); 495 + }); 496 + 497 + it('handles fetch exceptions without error interceptor', async () => { 498 + const client = createClient({ baseUrl: 'https://example.com' }); 499 + 500 + const abortError = new DOMException( 501 + 'The operation was aborted', 502 + 'AbortError', 503 + ); 504 + const mockFetch: MockFetch = vi.fn().mockRejectedValue(abortError); 505 + 506 + const result = await client.get({ 507 + fetch: mockFetch, 508 + url: '/test', 509 + }); 510 + 511 + expect(result.error).toBe(abortError); 512 + }); 513 + });
+34 -1
packages/openapi-ts/src/plugins/@hey-api/client-fetch/bundle/client.ts
··· 93 93 // fetch must be assigned here, otherwise it would throw the error: 94 94 // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation 95 95 const _fetch = opts.fetch!; 96 - let response = await _fetch(request); 96 + let response: Response; 97 + 98 + try { 99 + response = await _fetch(request); 100 + } catch (error) { 101 + // Handle fetch exceptions (AbortError, network errors, etc.) 102 + let finalError = error; 103 + 104 + for (const fn of interceptors.error.fns) { 105 + if (fn) { 106 + finalError = (await fn( 107 + error, 108 + undefined as any, 109 + request, 110 + opts, 111 + )) as unknown; 112 + } 113 + } 114 + 115 + finalError = finalError || ({} as unknown); 116 + 117 + if (opts.throwOnError) { 118 + throw finalError; 119 + } 120 + 121 + // Return error response 122 + return opts.responseStyle === 'data' 123 + ? undefined 124 + : { 125 + error: finalError, 126 + request, 127 + response: undefined as any, 128 + }; 129 + } 97 130 98 131 for (const fn of interceptors.response.fns) { 99 132 if (fn) {