Skip to content

Commit 8d2cce6

Browse files
committed
feature(error): Reworked error processing from Axios requests, adjusted error logging on some stream mode functions, added and updated tests.
1 parent 92ae275 commit 8d2cce6

File tree

4 files changed

+633
-71
lines changed

4 files changed

+633
-71
lines changed

src/api-client/base-client.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import {
1515
import {
1616
HTTPValidationError,
1717
GalileoAPIError,
18-
GalileoAPIStandardErrorData,
1918
isGalileoAPIStandardErrorData
2019
} from '../types/errors.types';
2120

Lines changed: 272 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import { http, HttpResponse } from 'msw';
22
import { setupServer } from 'msw/node';
3-
import { BaseClient, RequestMethod } from '../../src/api-client/base-client';
3+
import {
4+
BaseClient,
5+
GENERIC_ERROR_MESSAGE,
6+
RequestMethod
7+
} from '../../src/api-client/base-client';
48
import { Routes } from '../../src/types/routes.types';
59
import { getSdkIdentifier } from '../../src/utils/version';
10+
import { GalileoAPIError } from '../../src/types/errors.types';
11+
import type { GalileoAPIStandardErrorData } from '../../src/types/errors.types';
612

713
// Test implementation of BaseClient
814
class TestClient extends BaseClient {
@@ -18,28 +24,26 @@ class TestClient extends BaseClient {
1824
}
1925
}
2026

21-
describe('BaseClient Headers', () => {
22-
let capturedHeaders: Record<string, string> = {};
23-
24-
const server = setupServer(
25-
http.get('http://localhost:8088/healthcheck', ({ request }) => {
26-
// Capture headers from the request
27-
capturedHeaders = {};
28-
request.headers.forEach((value, key) => {
29-
capturedHeaders[key] = value;
30-
});
31-
32-
return HttpResponse.json({ status: 'ok' });
33-
})
34-
);
35-
36-
beforeAll(() => server.listen());
37-
afterEach(() => {
38-
server.resetHandlers();
27+
let capturedHeaders: Record<string, string> = {};
28+
29+
const server = setupServer(
30+
http.get('http://localhost:8088/healthcheck', ({ request }) => {
3931
capturedHeaders = {};
40-
});
41-
afterAll(() => server.close());
32+
request.headers.forEach((value, key) => {
33+
capturedHeaders[key] = value;
34+
});
35+
return HttpResponse.json({ status: 'ok' });
36+
})
37+
);
4238

39+
beforeAll(() => server.listen());
40+
afterEach(() => {
41+
server.resetHandlers();
42+
capturedHeaders = {};
43+
});
44+
afterAll(() => server.close());
45+
46+
describe('BaseClient Headers', () => {
4347
it('should include X-Galileo-SDK header with correct value', async () => {
4448
const client = new TestClient();
4549

@@ -62,21 +66,8 @@ describe('BaseClient Headers', () => {
6266
});
6367

6468
it('should include custom headers when provided', async () => {
65-
const server = setupServer(
66-
http.get('http://localhost:8088/healthcheck', ({ request }) => {
67-
capturedHeaders = {};
68-
request.headers.forEach((value, key) => {
69-
capturedHeaders[key] = value;
70-
});
71-
return HttpResponse.json({ status: 'ok' });
72-
})
73-
);
74-
75-
server.listen();
76-
7769
const client = new TestClient();
7870

79-
// Test with extra headers
8071
await client.makeRequest(
8172
RequestMethod.GET,
8273
Routes.healthCheck,
@@ -87,7 +78,253 @@ describe('BaseClient Headers', () => {
8778

8879
expect(capturedHeaders['custom-header']).toBe('custom-value');
8980
expect(capturedHeaders['x-galileo-sdk']).toBe(getSdkIdentifier());
81+
});
82+
});
83+
84+
/**
85+
* Catalog-aligned minimal: Orbit 1006 "Resource not found" required fields only.
86+
*/
87+
const VALID_STANDARD_ERROR: GalileoAPIStandardErrorData = {
88+
error_code: 1006,
89+
error_type: 'not_found_error',
90+
error_group: 'shared',
91+
severity: 'medium',
92+
message: 'The requested resource could not be found.',
93+
retriable: false,
94+
blocking: false
95+
};
96+
97+
/**
98+
* Catalog-aligned full: Orbit 1006 with optional fields (dataset not found variant).
99+
*/
100+
const FULL_STANDARD_ERROR: GalileoAPIStandardErrorData = {
101+
error_code: 1006,
102+
error_type: 'not_found_error',
103+
error_group: 'shared',
104+
severity: 'medium',
105+
message: 'Dataset with the given id was not found.',
106+
user_action: 'Verify the identifier and try again.',
107+
documentation_link: null,
108+
retriable: false,
109+
blocking: true,
110+
http_status_code: 404,
111+
source_service: 'api',
112+
context: { dataset_id: 'ds-123' }
113+
};
114+
115+
describe('BaseClient API error handling', () => {
116+
test('test makeRequest throws GalileoAPIError when response has valid standard_error', async () => {
117+
server.use(
118+
http.get('http://localhost:8088/healthcheck', () =>
119+
HttpResponse.json(
120+
{ standard_error: VALID_STANDARD_ERROR },
121+
{ status: 400 }
122+
)
123+
)
124+
);
125+
const client = new TestClient();
126+
127+
let err: unknown;
128+
try {
129+
await client.testRequest();
130+
} catch (e) {
131+
err = e;
132+
}
133+
expect(err).toBeInstanceOf(GalileoAPIError);
134+
const apiErr = err as GalileoAPIError;
135+
expect(apiErr.message).toBe(VALID_STANDARD_ERROR.message);
136+
expect(apiErr.errorCode).toBe(VALID_STANDARD_ERROR.error_code);
137+
expect(apiErr.retriable).toBe(VALID_STANDARD_ERROR.retriable);
138+
});
139+
140+
test('test makeRequest throws generic parse error when standard_error is present but invalid', async () => {
141+
server.use(
142+
http.get('http://localhost:8088/healthcheck', () =>
143+
HttpResponse.json({ standard_error: { message: 'x' } }, { status: 400 })
144+
)
145+
);
146+
const client = new TestClient();
147+
148+
await expect(client.testRequest()).rejects.toThrow(
149+
'The API returned an error, but the details could not be parsed.'
150+
);
151+
});
152+
153+
test('test makeRequest throws generic parse error when standard_error is null', async () => {
154+
server.use(
155+
http.get('http://localhost:8088/healthcheck', () =>
156+
HttpResponse.json({ standard_error: null }, { status: 400 })
157+
)
158+
);
159+
const client = new TestClient();
160+
161+
await expect(client.testRequest()).rejects.toThrow(
162+
'The API returned an error, but the details could not be parsed.'
163+
);
164+
});
165+
166+
test('test makeRequest throws generic parse error when standard_error is non-object', async () => {
167+
server.use(
168+
http.get('http://localhost:8088/healthcheck', () =>
169+
HttpResponse.json({ standard_error: 'invalid' }, { status: 400 })
170+
)
171+
);
172+
const client = new TestClient();
173+
174+
await expect(client.testRequest()).rejects.toThrow(
175+
'The API returned an error, but the details could not be parsed.'
176+
);
177+
});
178+
179+
test('test makeRequest throws generic parse error when standard_error is empty object', async () => {
180+
server.use(
181+
http.get('http://localhost:8088/healthcheck', () =>
182+
HttpResponse.json({ standard_error: {} }, { status: 400 })
183+
)
184+
);
185+
const client = new TestClient();
186+
187+
await expect(client.testRequest()).rejects.toThrow(
188+
/The API returned an error, but the details could not be parsed\./
189+
);
190+
});
191+
192+
test('test makeRequest throws generic parse error when standard_error is array', async () => {
193+
server.use(
194+
http.get('http://localhost:8088/healthcheck', () =>
195+
HttpResponse.json({ standard_error: [] }, { status: 400 })
196+
)
197+
);
198+
const client = new TestClient();
199+
200+
await expect(client.testRequest()).rejects.toThrow(
201+
/The API returned an error, but the details could not be parsed\./
202+
);
203+
});
204+
205+
test('test makeRequest throws generic parse error when standard_error has invalid optional type', async () => {
206+
server.use(
207+
http.get('http://localhost:8088/healthcheck', () =>
208+
HttpResponse.json(
209+
{
210+
standard_error: {
211+
...VALID_STANDARD_ERROR,
212+
documentation_link: 1
213+
}
214+
},
215+
{ status: 400 }
216+
)
217+
)
218+
);
219+
const client = new TestClient();
220+
221+
await expect(client.testRequest()).rejects.toThrow(
222+
/The API returned an error, but the details could not be parsed\./
223+
);
224+
});
225+
226+
test('test makeRequest throws GENERIC_ERROR_MESSAGE when error body is null', async () => {
227+
server.use(
228+
http.get('http://localhost:8088/healthcheck', () =>
229+
HttpResponse.json(null, { status: 500 })
230+
)
231+
);
232+
const client = new TestClient();
233+
234+
await expect(client.testRequest()).rejects.toThrow(GENERIC_ERROR_MESSAGE);
235+
});
236+
237+
test('test makeRequest throws GENERIC_ERROR_MESSAGE when error body is string', async () => {
238+
server.use(
239+
http.get('http://localhost:8088/healthcheck', () =>
240+
HttpResponse.json('error', { status: 500 })
241+
)
242+
);
243+
const client = new TestClient();
244+
245+
await expect(client.testRequest()).rejects.toThrow(GENERIC_ERROR_MESSAGE);
246+
});
247+
248+
test('test makeRequest throws GalileoAPIError with all mapped properties when response has valid standard_error', async () => {
249+
server.use(
250+
http.get('http://localhost:8088/healthcheck', () =>
251+
HttpResponse.json(
252+
{ standard_error: FULL_STANDARD_ERROR },
253+
{ status: 400 }
254+
)
255+
)
256+
);
257+
const client = new TestClient();
258+
259+
let err: unknown;
260+
try {
261+
await client.testRequest();
262+
} catch (e) {
263+
err = e;
264+
}
265+
expect(err).toBeInstanceOf(GalileoAPIError);
266+
const apiErr = err as GalileoAPIError;
267+
expect(apiErr.message).toBe(FULL_STANDARD_ERROR.message);
268+
expect(apiErr.errorCode).toBe(FULL_STANDARD_ERROR.error_code);
269+
expect(apiErr.errorType).toBe(FULL_STANDARD_ERROR.error_type);
270+
expect(apiErr.errorGroup).toBe(FULL_STANDARD_ERROR.error_group);
271+
expect(apiErr.severity).toBe(FULL_STANDARD_ERROR.severity);
272+
expect(apiErr.userAction).toBe(FULL_STANDARD_ERROR.user_action);
273+
expect(apiErr.documentationLink).toBe(
274+
FULL_STANDARD_ERROR.documentation_link
275+
);
276+
expect(apiErr.retriable).toBe(FULL_STANDARD_ERROR.retriable);
277+
expect(apiErr.blocking).toBe(FULL_STANDARD_ERROR.blocking);
278+
expect(apiErr.httpStatusCode).toBe(FULL_STANDARD_ERROR.http_status_code);
279+
expect(apiErr.sourceService).toBe(FULL_STANDARD_ERROR.source_service);
280+
expect(apiErr.context).toEqual(FULL_STANDARD_ERROR.context);
281+
});
282+
283+
test('test makeRequest throws with detail message when detail is string and statusCode present', async () => {
284+
server.use(
285+
http.get('http://localhost:8088/healthcheck', () =>
286+
HttpResponse.json({ detail: 'Validation failed' }, { status: 422 })
287+
)
288+
);
289+
const client = new TestClient();
290+
291+
await expect(client.testRequest()).rejects.toThrow(
292+
/non-ok status code 422 with output: Validation failed/
293+
);
294+
});
295+
296+
test('test makeRequest throws with detail message when detail is validation array', async () => {
297+
server.use(
298+
http.get('http://localhost:8088/healthcheck', () =>
299+
HttpResponse.json(
300+
{
301+
detail: [
302+
{
303+
loc: ['body', 'x'],
304+
msg: 'field required',
305+
type: 'value_error'
306+
}
307+
]
308+
},
309+
{ status: 422 }
310+
)
311+
)
312+
);
313+
const client = new TestClient();
314+
315+
await expect(client.testRequest()).rejects.toThrow(
316+
/non-ok status code 422 with output: field required/
317+
);
318+
});
319+
320+
test('test makeRequest throws GENERIC_ERROR_MESSAGE when error body has neither standard_error nor detail', async () => {
321+
server.use(
322+
http.get('http://localhost:8088/healthcheck', () =>
323+
HttpResponse.json({}, { status: 500 })
324+
)
325+
);
326+
const client = new TestClient();
90327

91-
server.close();
328+
await expect(client.testRequest()).rejects.toThrow(GENERIC_ERROR_MESSAGE);
92329
});
93330
});

0 commit comments

Comments
 (0)