Skip to content

Commit dcd2ee8

Browse files
zhongnansusipopo
authored andcommitted
[MD] Refactor data source server side error handling (opensearch-project#2661)
- Rename DataSourceConfigError to DataSourceError - Add helper createDataSourceError() to create data source error from, and replace usage of new DataSourceError(e) with createDataSourceError(e) - import createDataSourceError() from data source plugin setup interface, refactor to move data source error response logic from router.ts to resolve_index.ts. Signed-off-by: Su <szhongna@amazon.com> Signed-off-by: Sergey V. Osipov <sipopo@yandex.ru>
1 parent 89f7bf4 commit dcd2ee8

File tree

12 files changed

+184
-57
lines changed

12 files changed

+184
-57
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
5757
- Adding @zhongnansu as maintainer. ([#2590](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2590))
5858

5959
### 🪛 Refactoring
60+
* [MD] Refactor data source error handling ([#2661](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2661))
6061

6162
### 🔩 Tests
6263

src/core/server/http/router/router.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -301,21 +301,11 @@ export class Router implements IRouter {
301301
return e;
302302
}
303303

304-
if (isDataSourceConfigError(e)) {
305-
return hapiResponseAdapter.handle(
306-
opensearchDashboardsResponseFactory.badRequest({ body: e.message })
307-
);
308-
}
309-
310304
return hapiResponseAdapter.toInternalError();
311305
}
312306
}
313307
}
314308

315-
const isDataSourceConfigError = (error: any) => {
316-
return error.constructor.name === 'DataSourceConfigError' && error.statusCode === 400;
317-
};
318-
319309
const convertOpenSearchUnauthorized = (
320310
e: OpenSearchNotAuthorizedError
321311
): ErrorHttpResponseOptions => {

src/plugins/data/server/plugin.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from 'src/core/server';
3232
import { ExpressionsServerSetup } from 'src/plugins/expressions/server';
33+
import { DataSourcePluginSetup } from 'src/plugins/data_source/server';
3334
import { ConfigSchema } from '../config';
3435
import { IndexPatternsService, IndexPatternsServiceStart } from './index_patterns';
3536
import { ISearchSetup, ISearchStart, SearchEnhancements } from './search';
@@ -64,6 +65,7 @@ export interface DataPluginStart {
6465
export interface DataPluginSetupDependencies {
6566
expressions: ExpressionsServerSetup;
6667
usageCollection?: UsageCollectionSetup;
68+
dataSource?: DataSourcePluginSetup;
6769
}
6870

6971
// eslint-disable-next-line @typescript-eslint/no-empty-interface
@@ -96,7 +98,7 @@ export class DataServerPlugin
9698

9799
public setup(
98100
core: CoreSetup<DataPluginStartDependencies, DataPluginStart>,
99-
{ expressions, usageCollection }: DataPluginSetupDependencies
101+
{ expressions, usageCollection, dataSource }: DataPluginSetupDependencies
100102
) {
101103
this.indexPatterns.setup(core);
102104
this.scriptsService.setup(core);
@@ -109,6 +111,7 @@ export class DataServerPlugin
109111
const searchSetup = this.searchService.setup(core, {
110112
registerFunction: expressions.registerFunction,
111113
usageCollection,
114+
dataSource,
112115
});
113116

114117
return {

src/plugins/data/server/search/opensearch_search/opensearch_search_strategy.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { SharedGlobalConfig, Logger } from 'opensearch-dashboards/server';
3333
import { SearchResponse } from 'elasticsearch';
3434
import { Observable } from 'rxjs';
3535
import { ApiResponse } from '@opensearch-project/opensearch';
36+
import { DataSourcePluginSetup } from 'src/plugins/data_source/server';
3637
import { SearchUsage } from '../collectors/usage';
3738
import { toSnakeCase } from './to_snake_case';
3839
import {
@@ -47,7 +48,8 @@ import { decideClient } from './decide_client';
4748
export const opensearchSearchStrategyProvider = (
4849
config$: Observable<SharedGlobalConfig>,
4950
logger: Logger,
50-
usage?: SearchUsage
51+
usage?: SearchUsage,
52+
dataSource?: DataSourcePluginSetup
5153
): ISearchStrategy => {
5254
return {
5355
search: async (context, request, options) => {
@@ -88,6 +90,10 @@ export const opensearchSearchStrategyProvider = (
8890
};
8991
} catch (e) {
9092
if (usage) usage.trackError();
93+
94+
if (dataSource && request.dataSourceId) {
95+
throw dataSource.createDataSourceError(e);
96+
}
9197
throw e;
9298
}
9399
},

src/plugins/data/server/search/search_service.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
StartServicesAccessor,
4343
} from 'src/core/server';
4444
import { first } from 'rxjs/operators';
45+
import { DataSourcePluginSetup } from 'src/plugins/data_source/server';
4546
import { ISearchSetup, ISearchStart, ISearchStrategy, SearchEnhancements } from './types';
4647

4748
import { AggsService, AggsSetupDependencies } from './aggs';
@@ -78,6 +79,7 @@ type StrategyMap = Record<string, ISearchStrategy<any, any>>;
7879
export interface SearchServiceSetupDependencies {
7980
registerFunction: AggsSetupDependencies['registerFunction'];
8081
usageCollection?: UsageCollectionSetup;
82+
dataSource?: DataSourcePluginSetup;
8183
}
8284

8385
/** @internal */
@@ -105,7 +107,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
105107

106108
public setup(
107109
core: CoreSetup<{}, DataPluginStart>,
108-
{ registerFunction, usageCollection }: SearchServiceSetupDependencies
110+
{ registerFunction, usageCollection, dataSource }: SearchServiceSetupDependencies
109111
): ISearchSetup {
110112
const usage = usageCollection ? usageProvider(core, this.initializerContext) : undefined;
111113

@@ -122,7 +124,8 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
122124
opensearchSearchStrategyProvider(
123125
this.initializerContext.config.legacy.globalConfig$,
124126
this.logger,
125-
usage
127+
usage,
128+
dataSource
126129
)
127130
);
128131

src/plugins/data_source/server/client/configure_client.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
} from '../../common/data_sources';
1414
import { DataSourcePluginConfigType } from '../../config';
1515
import { CryptographyServiceSetup } from '../cryptography_service';
16-
import { DataSourceConfigError } from '../lib/error';
16+
import { createDataSourceError, DataSourceError } from '../lib/error';
1717
import { DataSourceClientParams } from '../types';
1818
import { parseClientOptions } from './client_config';
1919
import { OpenSearchClientPoolSetup } from './client_pool';
@@ -32,8 +32,8 @@ export const configureClient = async (
3232
} catch (error: any) {
3333
logger.error(`Failed to get data source client for dataSourceId: [${dataSourceId}]`);
3434
logger.error(error);
35-
// Re-throw as DataSourceConfigError
36-
throw new DataSourceConfigError('Failed to get data source client: ', error);
35+
// Re-throw as DataSourceError
36+
throw createDataSourceError(error);
3737
}
3838
};
3939

@@ -59,8 +59,8 @@ export const getCredential = async (
5959
const { decryptedText, encryptionContext } = await cryptography
6060
.decodeAndDecrypt(password)
6161
.catch((err: any) => {
62-
// Re-throw as DataSourceConfigError
63-
throw new DataSourceConfigError('Unable to decrypt "auth.credentials.password".', err);
62+
// Re-throw as DataSourceError
63+
throw createDataSourceError(err);
6464
});
6565

6666
if (encryptionContext!.endpoint !== endpoint) {

src/plugins/data_source/server/legacy/configure_legacy_client.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { CryptographyServiceSetup } from '../cryptography_service';
2323
import { DataSourceClientParams, LegacyClientCallAPIParams } from '../types';
2424
import { OpenSearchClientPoolSetup, getCredential, getDataSource } from '../client';
2525
import { parseClientOptions } from './client_config';
26-
import { DataSourceConfigError } from '../lib/error';
26+
import { createDataSourceError, DataSourceError } from '../lib/error';
2727

2828
export const configureLegacyClient = async (
2929
{ dataSourceId, savedObjects, cryptography }: DataSourceClientParams,
@@ -40,8 +40,8 @@ export const configureLegacyClient = async (
4040
} catch (error: any) {
4141
logger.error(`Failed to get data source client for dataSourceId: [${dataSourceId}]`);
4242
logger.error(error);
43-
// Re-throw as DataSourceConfigError
44-
throw new DataSourceConfigError('Failed to get data source client: ', error);
43+
// Re-throw as DataSourceError
44+
throw createDataSourceError(error);
4545
}
4646
};
4747

@@ -140,7 +140,6 @@ const callAPI = async (
140140
if (!options.wrap401Errors || err.statusCode !== 401) {
141141
throw err;
142142
}
143-
144143
throw LegacyOpenSearchErrorHelpers.decorateNotAuthorizedError(err);
145144
}
146145
};

src/plugins/data_source/server/lib/error.test.ts

Lines changed: 94 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,118 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6+
import { ApiResponse } from '@opensearch-project/opensearch';
7+
import {
8+
ConnectionError,
9+
NoLivingConnectionsError,
10+
ResponseError,
11+
} from '@opensearch-project/opensearch/lib/errors';
612
import { SavedObjectsErrorHelpers } from '../../../../../src/core/server';
7-
import { DataSourceConfigError } from './error';
13+
import { createDataSourceError, DataSourceError } from './error';
14+
import { errors as LegacyErrors } from 'elasticsearch';
815

9-
describe('DataSourceConfigError', () => {
10-
it('create from savedObject bad request error should be 400 error', () => {
16+
const createApiResponseError = ({
17+
statusCode = 200,
18+
headers = {},
19+
body = {},
20+
}: {
21+
statusCode?: number;
22+
headers?: Record<string, string>;
23+
body?: Record<string, any>;
24+
} = {}): ApiResponse => {
25+
return {
26+
body,
27+
statusCode,
28+
headers,
29+
warnings: [],
30+
meta: {} as any,
31+
};
32+
};
33+
34+
describe('CreateDataSourceError', () => {
35+
it('create from savedObject bad request error should generate 400 error', () => {
1136
const error = SavedObjectsErrorHelpers.createBadRequestError('test reason message');
12-
expect(new DataSourceConfigError('test prefix: ', error)).toMatchObject({
37+
expect(createDataSourceError(error)).toMatchObject({
1338
statusCode: 400,
14-
message: 'test prefix: test reason message: Bad Request',
39+
message: 'Data Source Error: test reason message: Bad Request',
1540
});
1641
});
1742

18-
it('create from savedObject not found error should be 400 error', () => {
19-
const error = SavedObjectsErrorHelpers.decorateNotAuthorizedError(new Error());
20-
expect(new DataSourceConfigError('test prefix: ', error)).toHaveProperty('statusCode', 400);
43+
it('create from savedObject not found error should have statusCode 404', () => {
44+
const error = SavedObjectsErrorHelpers.createGenericNotFoundError('type', 'id');
45+
expect(createDataSourceError(error)).toHaveProperty('statusCode', 404);
2146
});
2247

23-
it('create from savedObject service unavailable error should be a 500 error', () => {
48+
it('create from savedObject service unavailable error should have statusCode 503', () => {
2449
const error = SavedObjectsErrorHelpers.decorateOpenSearchUnavailableError(
2550
new Error('test reason message')
2651
);
27-
expect(new DataSourceConfigError('test prefix: ', error)).toMatchObject({
28-
statusCode: 500,
29-
message: 'test prefix: test reason message',
52+
expect(createDataSourceError(error)).toMatchObject({
53+
statusCode: 503,
54+
message: 'Data Source Error: test reason message',
3055
});
3156
});
3257

3358
it('create from non savedObject error should always be a 400 error', () => {
3459
const error = new Error('test reason message');
35-
expect(new DataSourceConfigError('test prefix: ', error)).toMatchObject({
60+
expect(createDataSourceError(error)).toMatchObject({
3661
statusCode: 400,
37-
message: 'test prefix: test reason message',
62+
message: 'Data Source Error: test reason message',
3863
});
3964
});
65+
66+
it('create from client response error 401 should be casted to a 400 DataSourceError', () => {
67+
expect(
68+
createDataSourceError(new ResponseError(createApiResponseError({ statusCode: 401 })))
69+
).toHaveProperty('statusCode', 400);
70+
});
71+
72+
it('create from non 401 client response error should respect original statusCode', () => {
73+
expect(
74+
createDataSourceError(new ResponseError(createApiResponseError({ statusCode: 403 })))
75+
).toHaveProperty('statusCode', 403);
76+
expect(
77+
createDataSourceError(new ResponseError(createApiResponseError({ statusCode: 404 })))
78+
).toHaveProperty('statusCode', 404);
79+
expect(
80+
createDataSourceError(new ResponseError(createApiResponseError({ statusCode: 500 })))
81+
).toHaveProperty('statusCode', 500);
82+
});
83+
84+
it('create from non-response client error should be casted to a 400 DataSourceError', () => {
85+
expect(
86+
createDataSourceError(new ConnectionError('error', createApiResponseError()))
87+
).toHaveProperty('statusCode', 400);
88+
expect(
89+
createDataSourceError(new NoLivingConnectionsError('error', createApiResponseError()))
90+
).toHaveProperty('statusCode', 400);
91+
expect(createDataSourceError(new Error('foo'))).toHaveProperty('statusCode', 400);
92+
});
93+
94+
it('create from legacy client 401 error should be casted to a 400 DataSourceError', () => {
95+
expect(createDataSourceError(new LegacyErrors.AuthenticationException())).toEqual(
96+
new DataSourceError(new Error('dummy'), 'Authentication Exception', 400)
97+
);
98+
});
99+
100+
it('create from legacy client non 401 error should respect original statusCode', () => {
101+
expect(createDataSourceError(new LegacyErrors.NotFound())).toEqual(
102+
new DataSourceError(new Error('dummy'), 'Not Found', 404)
103+
);
104+
expect(createDataSourceError(new LegacyErrors.TooManyRequests())).toEqual(
105+
new DataSourceError(new Error('dummy'), 'Too Many Requests', 429)
106+
);
107+
expect(createDataSourceError(new LegacyErrors.InternalServerError())).toEqual(
108+
new DataSourceError(new Error('dummy'), 'Internal Server Error', 400)
109+
);
110+
});
111+
112+
it('create from legacy client error should be casted to a 400 DataSourceError', () => {
113+
expect(createDataSourceError(new LegacyErrors.NoConnections())).toEqual(
114+
new DataSourceError(new Error('dummy'), 'No Living connections', 400)
115+
);
116+
expect(createDataSourceError(new LegacyErrors.ConnectionFault())).toEqual(
117+
new DataSourceError(new Error('dummy'), 'Connection Failure', 400)
118+
);
119+
});
40120
});

src/plugins/data_source/server/lib/error.ts

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,47 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6+
import { ResponseError } from '@opensearch-project/opensearch/lib/errors';
7+
import { errors as LegacyErrors } from 'elasticsearch';
68
import { SavedObjectsErrorHelpers } from '../../../../../src/core/server';
7-
import { OsdError } from '../../../../../src/plugins/opensearch_dashboards_utils/common';
9+
import { OsdError } from '../../../opensearch_dashboards_utils/common';
810

9-
export class DataSourceConfigError extends OsdError {
11+
export class DataSourceError extends OsdError {
1012
// must have statusCode to avoid route handler in search.ts to return 500
1113
statusCode: number;
12-
constructor(messagePrefix: string, error: any) {
13-
const messageContent = SavedObjectsErrorHelpers.isSavedObjectsClientError(error)
14-
? error.output.payload.message
15-
: error.message;
16-
super(messagePrefix + messageContent);
17-
// Cast all 5xx error returned by saveObjectClient to 500.
18-
// Cast both savedObject client 4xx errors, and other errors to 400
19-
this.statusCode = SavedObjectsErrorHelpers.isOpenSearchUnavailableError(error) ? 500 : 400;
14+
constructor(error: any, message?: string, statusCode?: number) {
15+
message = message ? message : error.message;
16+
super('Data Source Error: ' + message);
17+
if (statusCode) {
18+
this.statusCode = statusCode;
19+
} else if (error.statusCode) {
20+
this.statusCode = error.statusCode;
21+
} else {
22+
this.statusCode = 400;
23+
}
2024
}
2125
}
26+
27+
export const createDataSourceError = (error: any, message?: string): DataSourceError => {
28+
// handle saved object client error, while retrieve data source meta info
29+
if (SavedObjectsErrorHelpers.isSavedObjectsClientError(error)) {
30+
return new DataSourceError(error, error.output.payload.message, error.output.statusCode);
31+
}
32+
33+
// cast OpenSearch client 401 response error to 400, due to https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2591
34+
if (isResponseError(error) && error.statusCode === 401) {
35+
return new DataSourceError(error, JSON.stringify(error.meta.body), 400);
36+
}
37+
38+
// cast legacy client 401 response error to 400
39+
if (error instanceof LegacyErrors.AuthenticationException) {
40+
return new DataSourceError(error, error.message, 400);
41+
}
42+
43+
// handle all other error that may or may not comes with statuscode
44+
return new DataSourceError(error, message);
45+
};
46+
47+
const isResponseError = (error: any): error is ResponseError => {
48+
return Boolean(error.body && error.statusCode && error.headers);
49+
};

src/plugins/data_source/server/plugin.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { DATA_SOURCE_SAVED_OBJECT_TYPE } from '../common';
2828

2929
// eslint-disable-next-line @osd/eslint/no-restricted-paths
3030
import { ensureRawRequest } from '../../../../src/core/server/http/router';
31+
import { createDataSourceError } from './lib/error';
3132
export class DataSourcePlugin implements Plugin<DataSourcePluginSetup, DataSourcePluginStart> {
3233
private readonly logger: Logger;
3334
private readonly cryptographyService: CryptographyService;
@@ -102,7 +103,9 @@ export class DataSourcePlugin implements Plugin<DataSourcePluginSetup, DataSourc
102103
)
103104
);
104105

105-
return {};
106+
return {
107+
createDataSourceError: (e: any) => createDataSourceError(e),
108+
};
106109
}
107110

108111
public start(core: CoreStart) {

0 commit comments

Comments
 (0)