From 3b7c4b73607f40a74f31f39491025c63bc92e40e Mon Sep 17 00:00:00 2001 From: Jaroslav Hejlek Date: Wed, 26 Jun 2024 15:10:50 +0200 Subject: [PATCH] feat: added data property to API error object (#559) This update is needed because in API we will be adding a new data field to the API error, which will be used during dataset validation. https://github.com/apify/apify-docs/pull/1079 --------- Co-authored-by: Jaroslav Hejlek --- src/apify_api_error.ts | 9 ++++ test/apify_api_error.test.js | 61 ++++++++++++++++++++++++++- test/mock_server/routes/add_routes.js | 15 ++++++- 3 files changed, 82 insertions(+), 3 deletions(-) diff --git a/src/apify_api_error.ts b/src/apify_api_error.ts index 5f16c5b7..b9a47388 100644 --- a/src/apify_api_error.ts +++ b/src/apify_api_error.ts @@ -61,6 +61,11 @@ export class ApifyApiError extends Error { */ originalStack: string; + /** + * Additional data provided by the API about the error + */ + data?: Record; + /** * @hidden */ @@ -68,6 +73,7 @@ export class ApifyApiError extends Error { let message!: string; let type: string | undefined; let responseData = response.data; + let errorData: Record | undefined; // Some methods (e.g. downloadItems) set up forceBuffer on request response. If this request failed // the body buffer needs to parse to get the correct error. @@ -83,6 +89,7 @@ export class ApifyApiError extends Error { const { error } = responseData; message = error.message; type = error.type; + errorData = error.data; } else if (responseData) { let dataString; try { @@ -106,6 +113,8 @@ export class ApifyApiError extends Error { this.originalStack = stack.slice(stack.indexOf('\n')); this.stack = this._createApiStack(); + + this.data = errorData; } private _safelyParsePathFromResponse(response: AxiosResponse) { diff --git a/test/apify_api_error.test.js b/test/apify_api_error.test.js index 0967f235..653fb32a 100644 --- a/test/apify_api_error.test.js +++ b/test/apify_api_error.test.js @@ -1,15 +1,22 @@ -const { Browser } = require('./_helper'); +const { Browser, DEFAULT_OPTIONS } = require('./_helper'); +const mockServer = require('./mock_server/server'); const { ApifyClient } = require('../src/index'); describe('ApifyApiError', () => { + let baseUrl; const browser = new Browser(); beforeAll(async () => { await browser.start(); + const server = await mockServer.start(); + baseUrl = `http://localhost:${server.address().port}`; }); afterAll(async () => { - await browser.cleanUpBrowser(); + await Promise.all([ + mockServer.close(), + browser.cleanUpBrowser(), + ]); }); test('should carry all the information', async () => { @@ -62,4 +69,54 @@ describe('ApifyApiError', () => { expect(error.httpMethod).toEqual('get'); expect(error.attempt).toEqual(1); }); + + test('should carry additional error data if provided', async () => { + const datasetId = '400'; // check add_routes.js to see details of this mock + const data = JSON.stringify([{ someData: 'someValue' }, { someData: 'someValue' }]); + const clientConfig = { + baseUrl, + maxRetries: 0, + ...DEFAULT_OPTIONS, + }; + + // Works in node + try { + const client = new ApifyClient(clientConfig); + await client.dataset(datasetId).pushItems(data); + throw new Error('wrong error'); + } catch (err) { + expect(err.name).toEqual('ApifyApiError'); + expect(err.type).toEqual('schema-validation-error'); + expect(err.data).toEqual({ + invalidItems: { + 0: [`should have required property 'name'`], + }, + }); + } + + // Works in browser + const page = await browser.getInjectedPage(); + const error = await page.evaluate(async (cConfig, dId, d) => { + const client = new window.Apify.ApifyClient(cConfig); + const datasetClient = client.dataset(dId); + try { + await datasetClient.pushItems(d); + throw new Error('wrong error'); + } catch (err) { + const serializableErr = {}; + Object.getOwnPropertyNames(err).forEach((prop) => { + serializableErr[prop] = err[prop]; + }); + serializableErr.resourcePath = datasetClient.resourcePath; + return serializableErr; + } + }, clientConfig, datasetId, data); + expect(error.name).toEqual('ApifyApiError'); + expect(error.type).toEqual('schema-validation-error'); + expect(error.data).toEqual({ + invalidItems: { + 0: [`should have required property 'name'`], + }, + }); + }); }); diff --git a/test/mock_server/routes/add_routes.js b/test/mock_server/routes/add_routes.js index c0d6af09..cac89822 100644 --- a/test/mock_server/routes/add_routes.js +++ b/test/mock_server/routes/add_routes.js @@ -40,7 +40,20 @@ const HANDLERS = { let payload = {}; if (responseStatusCode === 200) payload = { data: { id } }; else if (responseStatusCode === 204) payload = null; - else if (responseStatusCode === 404) { + else if (responseStatusCode === 400) { + // This is not ideal, what if we have more endpoints which can return 400? + payload = { + error: { + type: 'schema-validation-error', + message: 'Schema validation failed', + data: { + invalidItems: { + 0: [`should have required property 'name'`], + }, + }, + }, + }; + } else if (responseStatusCode === 404) { payload = { error: { type: 'record-not-found',