Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MD] Refactor data source server side error handling #2661

Merged
merged 7 commits into from
Oct 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
* Adding @zhongnansu as maintainer. ([#2590](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2590))

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

### 🔩 Tests
* [Multi DataSource] Add unit test coverage for Update Data source management stack ([#2567](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2567))
Expand Down
10 changes: 0 additions & 10 deletions src/core/server/http/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,21 +301,11 @@ export class Router implements IRouter {
return e;
}

if (isDataSourceConfigError(e)) {
return hapiResponseAdapter.handle(
opensearchDashboardsResponseFactory.badRequest({ body: e.message })
);
}

return hapiResponseAdapter.toInternalError();
}
}
}

const isDataSourceConfigError = (error: any) => {
return error.constructor.name === 'DataSourceConfigError' && error.statusCode === 400;
};

const convertOpenSearchUnauthorized = (
e: OpenSearchNotAuthorizedError
): ErrorHttpResponseOptions => {
Expand Down
5 changes: 4 additions & 1 deletion src/plugins/data/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from 'src/core/server';
import { ExpressionsServerSetup } from 'src/plugins/expressions/server';
import { DataSourcePluginSetup } from 'src/plugins/data_source/server';
import { ConfigSchema } from '../config';
import { IndexPatternsService, IndexPatternsServiceStart } from './index_patterns';
import { ISearchSetup, ISearchStart, SearchEnhancements } from './search';
Expand Down Expand Up @@ -64,6 +65,7 @@ export interface DataPluginStart {
export interface DataPluginSetupDependencies {
expressions: ExpressionsServerSetup;
usageCollection?: UsageCollectionSetup;
dataSource?: DataSourcePluginSetup;
}

// eslint-disable-next-line @typescript-eslint/no-empty-interface
Expand Down Expand Up @@ -96,7 +98,7 @@ export class DataServerPlugin

public setup(
core: CoreSetup<DataPluginStartDependencies, DataPluginStart>,
{ expressions, usageCollection }: DataPluginSetupDependencies
{ expressions, usageCollection, dataSource }: DataPluginSetupDependencies
) {
this.indexPatterns.setup(core);
this.scriptsService.setup(core);
Expand All @@ -109,6 +111,7 @@ export class DataServerPlugin
const searchSetup = this.searchService.setup(core, {
registerFunction: expressions.registerFunction,
usageCollection,
dataSource,
});

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { SharedGlobalConfig, Logger } from 'opensearch-dashboards/server';
import { SearchResponse } from 'elasticsearch';
import { Observable } from 'rxjs';
import { ApiResponse } from '@opensearch-project/opensearch';
import { DataSourcePluginSetup } from 'src/plugins/data_source/server';
import { SearchUsage } from '../collectors/usage';
import { toSnakeCase } from './to_snake_case';
import {
Expand All @@ -47,7 +48,8 @@ import { decideClient } from './decide_client';
export const opensearchSearchStrategyProvider = (
config$: Observable<SharedGlobalConfig>,
logger: Logger,
usage?: SearchUsage
usage?: SearchUsage,
dataSource?: DataSourcePluginSetup
): ISearchStrategy => {
return {
search: async (context, request, options) => {
Expand Down Expand Up @@ -88,6 +90,10 @@ export const opensearchSearchStrategyProvider = (
};
} catch (e) {
if (usage) usage.trackError();

if (dataSource && request.dataSourceId) {
throw dataSource.createDataSourceError(e);
}
throw e;
}
},
Expand Down
7 changes: 5 additions & 2 deletions src/plugins/data/server/search/search_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
StartServicesAccessor,
} from 'src/core/server';
import { first } from 'rxjs/operators';
import { DataSourcePluginSetup } from 'src/plugins/data_source/server';
import { ISearchSetup, ISearchStart, ISearchStrategy, SearchEnhancements } from './types';

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

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

public setup(
core: CoreSetup<{}, DataPluginStart>,
{ registerFunction, usageCollection }: SearchServiceSetupDependencies
{ registerFunction, usageCollection, dataSource }: SearchServiceSetupDependencies
): ISearchSetup {
const usage = usageCollection ? usageProvider(core, this.initializerContext) : undefined;

Expand All @@ -122,7 +124,8 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
opensearchSearchStrategyProvider(
this.initializerContext.config.legacy.globalConfig$,
this.logger,
usage
usage,
dataSource
)
);

Expand Down
10 changes: 5 additions & 5 deletions src/plugins/data_source/server/client/configure_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from '../../common/data_sources';
import { DataSourcePluginConfigType } from '../../config';
import { CryptographyServiceSetup } from '../cryptography_service';
import { DataSourceConfigError } from '../lib/error';
import { createDataSourceError, DataSourceError } from '../lib/error';
import { DataSourceClientParams } from '../types';
import { parseClientOptions } from './client_config';
import { OpenSearchClientPoolSetup } from './client_pool';
Expand All @@ -32,8 +32,8 @@ export const configureClient = async (
} catch (error: any) {
logger.error(`Failed to get data source client for dataSourceId: [${dataSourceId}]`);
logger.error(error);
// Re-throw as DataSourceConfigError
throw new DataSourceConfigError('Failed to get data source client: ', error);
// Re-throw as DataSourceError
throw createDataSourceError(error);
}
};

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

if (encryptionContext!.endpoint !== endpoint) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { CryptographyServiceSetup } from '../cryptography_service';
import { DataSourceClientParams, LegacyClientCallAPIParams } from '../types';
import { OpenSearchClientPoolSetup, getCredential, getDataSource } from '../client';
import { parseClientOptions } from './client_config';
import { DataSourceConfigError } from '../lib/error';
import { createDataSourceError, DataSourceError } from '../lib/error';

export const configureLegacyClient = async (
{ dataSourceId, savedObjects, cryptography }: DataSourceClientParams,
Expand All @@ -40,8 +40,8 @@ export const configureLegacyClient = async (
} catch (error: any) {
logger.error(`Failed to get data source client for dataSourceId: [${dataSourceId}]`);
logger.error(error);
// Re-throw as DataSourceConfigError
throw new DataSourceConfigError('Failed to get data source client: ', error);
// Re-throw as DataSourceError
throw createDataSourceError(error);
}
};

Expand Down Expand Up @@ -140,7 +140,6 @@ const callAPI = async (
if (!options.wrap401Errors || err.statusCode !== 401) {
throw err;
}

throw LegacyOpenSearchErrorHelpers.decorateNotAuthorizedError(err);
}
};
Expand Down
108 changes: 94 additions & 14 deletions src/plugins/data_source/server/lib/error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,118 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { ApiResponse } from '@opensearch-project/opensearch';
import {
ConnectionError,
NoLivingConnectionsError,
ResponseError,
} from '@opensearch-project/opensearch/lib/errors';
import { SavedObjectsErrorHelpers } from '../../../../../src/core/server';
import { DataSourceConfigError } from './error';
import { createDataSourceError, DataSourceError } from './error';
import { errors as LegacyErrors } from 'elasticsearch';

describe('DataSourceConfigError', () => {
it('create from savedObject bad request error should be 400 error', () => {
const createApiResponseError = ({
statusCode = 200,
headers = {},
body = {},
}: {
statusCode?: number;
headers?: Record<string, string>;
body?: Record<string, any>;
} = {}): ApiResponse => {
return {
body,
statusCode,
headers,
warnings: [],
meta: {} as any,
};
};

describe('CreateDataSourceError', () => {
it('create from savedObject bad request error should generate 400 error', () => {
const error = SavedObjectsErrorHelpers.createBadRequestError('test reason message');
expect(new DataSourceConfigError('test prefix: ', error)).toMatchObject({
expect(createDataSourceError(error)).toMatchObject({
statusCode: 400,
message: 'test prefix: test reason message: Bad Request',
message: 'Data Source Error: test reason message: Bad Request',
});
});

it('create from savedObject not found error should be 400 error', () => {
const error = SavedObjectsErrorHelpers.decorateNotAuthorizedError(new Error());
expect(new DataSourceConfigError('test prefix: ', error)).toHaveProperty('statusCode', 400);
it('create from savedObject not found error should have statusCode 404', () => {
const error = SavedObjectsErrorHelpers.createGenericNotFoundError('type', 'id');
expect(createDataSourceError(error)).toHaveProperty('statusCode', 404);
});

it('create from savedObject service unavailable error should be a 500 error', () => {
it('create from savedObject service unavailable error should have statusCode 503', () => {
const error = SavedObjectsErrorHelpers.decorateOpenSearchUnavailableError(
new Error('test reason message')
);
expect(new DataSourceConfigError('test prefix: ', error)).toMatchObject({
statusCode: 500,
message: 'test prefix: test reason message',
expect(createDataSourceError(error)).toMatchObject({
statusCode: 503,
message: 'Data Source Error: test reason message',
});
});

it('create from non savedObject error should always be a 400 error', () => {
const error = new Error('test reason message');
expect(new DataSourceConfigError('test prefix: ', error)).toMatchObject({
expect(createDataSourceError(error)).toMatchObject({
statusCode: 400,
message: 'test prefix: test reason message',
message: 'Data Source Error: test reason message',
});
});

it('create from client response error 401 should be casted to a 400 DataSourceError', () => {
expect(
createDataSourceError(new ResponseError(createApiResponseError({ statusCode: 401 })))
).toHaveProperty('statusCode', 400);
});

it('create from non 401 client response error should respect original statusCode', () => {
expect(
createDataSourceError(new ResponseError(createApiResponseError({ statusCode: 403 })))
).toHaveProperty('statusCode', 403);
expect(
createDataSourceError(new ResponseError(createApiResponseError({ statusCode: 404 })))
).toHaveProperty('statusCode', 404);
expect(
createDataSourceError(new ResponseError(createApiResponseError({ statusCode: 500 })))
).toHaveProperty('statusCode', 500);
});

it('create from non-response client error should be casted to a 400 DataSourceError', () => {
expect(
createDataSourceError(new ConnectionError('error', createApiResponseError()))
).toHaveProperty('statusCode', 400);
expect(
createDataSourceError(new NoLivingConnectionsError('error', createApiResponseError()))
).toHaveProperty('statusCode', 400);
expect(createDataSourceError(new Error('foo'))).toHaveProperty('statusCode', 400);
});

it('create from legacy client 401 error should be casted to a 400 DataSourceError', () => {
expect(createDataSourceError(new LegacyErrors.AuthenticationException())).toEqual(
new DataSourceError(new Error('dummy'), 'Authentication Exception', 400)
);
});

it('create from legacy client non 401 error should respect original statusCode', () => {
expect(createDataSourceError(new LegacyErrors.NotFound())).toEqual(
new DataSourceError(new Error('dummy'), 'Not Found', 404)
);
expect(createDataSourceError(new LegacyErrors.TooManyRequests())).toEqual(
new DataSourceError(new Error('dummy'), 'Too Many Requests', 429)
);
expect(createDataSourceError(new LegacyErrors.InternalServerError())).toEqual(
new DataSourceError(new Error('dummy'), 'Internal Server Error', 400)
);
});

it('create from legacy client error should be casted to a 400 DataSourceError', () => {
expect(createDataSourceError(new LegacyErrors.NoConnections())).toEqual(
new DataSourceError(new Error('dummy'), 'No Living connections', 400)
);
expect(createDataSourceError(new LegacyErrors.ConnectionFault())).toEqual(
new DataSourceError(new Error('dummy'), 'Connection Failure', 400)
);
});
});
48 changes: 38 additions & 10 deletions src/plugins/data_source/server/lib/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,47 @@
* SPDX-License-Identifier: Apache-2.0
*/

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

export class DataSourceConfigError extends OsdError {
export class DataSourceError extends OsdError {
// must have statusCode to avoid route handler in search.ts to return 500
statusCode: number;
constructor(messagePrefix: string, error: any) {
const messageContent = SavedObjectsErrorHelpers.isSavedObjectsClientError(error)
? error.output.payload.message
: error.message;
super(messagePrefix + messageContent);
// Cast all 5xx error returned by saveObjectClient to 500.
// Cast both savedObject client 4xx errors, and other errors to 400
this.statusCode = SavedObjectsErrorHelpers.isOpenSearchUnavailableError(error) ? 500 : 400;
constructor(error: any, message?: string, statusCode?: number) {
message = message ? message : error.message;
super('Data Source Error: ' + message);
if (statusCode) {
this.statusCode = statusCode;
} else if (error.statusCode) {
this.statusCode = error.statusCode;
} else {
this.statusCode = 400;
}
}
}

export const createDataSourceError = (error: any, message?: string): DataSourceError => {
// handle saved object client error, while retrieve data source meta info
if (SavedObjectsErrorHelpers.isSavedObjectsClientError(error)) {
return new DataSourceError(error, error.output.payload.message, error.output.statusCode);
}

// cast OpenSearch client 401 response error to 400, due to https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2591
if (isResponseError(error) && error.statusCode === 401) {
return new DataSourceError(error, JSON.stringify(error.meta.body), 400);
}

// cast legacy client 401 response error to 400
if (error instanceof LegacyErrors.AuthenticationException) {
return new DataSourceError(error, error.message, 400);
}

// handle all other error that may or may not comes with statuscode
return new DataSourceError(error, message);
};

const isResponseError = (error: any): error is ResponseError => {
return Boolean(error.body && error.statusCode && error.headers);
};
5 changes: 4 additions & 1 deletion src/plugins/data_source/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { DATA_SOURCE_SAVED_OBJECT_TYPE } from '../common';

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

return {};
return {
createDataSourceError: (e: any) => createDataSourceError(e),
};
}

public start(core: CoreStart) {
Expand Down
Loading