diff --git a/__tests__/integration/api-error.test.ts b/__tests__/integration/api-error.test.ts new file mode 100644 index 0000000..8e8087e --- /dev/null +++ b/__tests__/integration/api-error.test.ts @@ -0,0 +1,69 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { ApiClient } from '../../src/ApiClient'; +import { StreamError } from '../../src/types'; + +const createApiClient = () => + new ApiClient({ + apiKey: 'test-api-key', + token: 'test-token', + baseUrl: 'https://example.com', + timeout: 3000, + }); + +const sendRequestWithResponse = (response: Response) => { + vi.spyOn(globalThis, 'fetch').mockResolvedValue(response); + return createApiClient() + .sendRequest('POST', '/test', undefined, undefined, {}) + .catch((e: unknown) => e); +}; + +describe('API errors', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('keeps the parsed error body on the thrown StreamError', async () => { + const body = { + code: 4, + message: + 'RewindChannel failed with error: "expected_last_message_id is stale"', + StatusCode: 409, + duration: '0.42ms', + more_info: 'https://getstream.io/chat/docs/api_errors_response', + details: [], + exception_fields: { last_message_id: 'stale' }, + }; + + const error = await sendRequestWithResponse( + new Response(JSON.stringify(body), { + status: 409, + headers: { 'Content-Type': 'application/json' }, + }), + ); + + expect(error).toBeInstanceOf(StreamError); + const streamError = error as StreamError; + expect(streamError.message).toBe(`Stream error code 4: ${body.message}`); + expect(streamError.code).toBe(4); + expect(streamError.metadata.responseCode).toBe(409); + expect(streamError.apiError).toEqual(body); + expect(streamError.apiError?.exception_fields).toEqual({ + last_message_id: 'stale', + }); + }); + + it('leaves apiError undefined when the error body is not JSON', async () => { + const error = await sendRequestWithResponse( + new Response('Bad Gateway', { + status: 502, + statusText: 'Bad Gateway', + }), + ); + + expect(error).toBeInstanceOf(StreamError); + const streamError = error as StreamError; + expect(streamError.message).toBe('Stream error: 502 - Bad Gateway'); + expect(streamError.code).toBe(502); + expect(streamError.apiError).toBeUndefined(); + }); +}); diff --git a/src/ApiClient.ts b/src/ApiClient.ts index d07050f..dd801e0 100644 --- a/src/ApiClient.ts +++ b/src/ApiClient.ts @@ -89,6 +89,7 @@ export class ApiClient { metadata, error!.code, undefined, + error!, ); } diff --git a/src/types.ts b/src/types.ts index e3bbeee..d87ca32 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,5 @@ +import type { APIError } from './gen/models'; + export type OmitTypeId = Omit; export interface ApiConfig { @@ -27,6 +29,12 @@ export class StreamError extends Error { public metadata: Partial, public code?: number, errorOptions?: ErrorOptions, + /** + * The error body the API returned (`exception_fields`, `details`, + * `more_info`, `status_code`, ...). Undefined when the request failed + * before a response arrived or the response body was not JSON. + */ + public apiError?: APIError, ) { super(message, errorOptions); }