diff --git a/src/plugins/data/common/search/expressions/eql.ts b/src/plugins/data/common/search/expressions/eql.ts index f82f443ea00b9..7caaa0c090466 100644 --- a/src/plugins/data/common/search/expressions/eql.ts +++ b/src/plugins/data/common/search/expressions/eql.ts @@ -166,7 +166,7 @@ export const getEqlFn = ({ body: response.rawResponse, }; } catch (e) { - request.error({ json: e }); + request.error({ json: 'attributes' in e ? e.attributes : { message: e.message } }); throw e; } }, diff --git a/src/plugins/data/common/search/expressions/esdsl.ts b/src/plugins/data/common/search/expressions/esdsl.ts index 34a67223b4be5..a18e1e3240050 100644 --- a/src/plugins/data/common/search/expressions/esdsl.ts +++ b/src/plugins/data/common/search/expressions/esdsl.ts @@ -188,7 +188,7 @@ export const getEsdslFn = ({ body: rawResponse, }; } catch (e) { - request.error({ json: e }); + request.error({ json: 'attributes' in e ? e.attributes : { message: e.message } }); throw e; } }, diff --git a/src/plugins/data/common/search/expressions/esql.ts b/src/plugins/data/common/search/expressions/esql.ts index ba6600ba0039e..30bf10a0f1bb7 100644 --- a/src/plugins/data/common/search/expressions/esql.ts +++ b/src/plugins/data/common/search/expressions/esql.ts @@ -227,7 +227,9 @@ export const getEsqlFn = ({ getStartDependencies }: EsqlFnArguments) => { .ok({ json: rawResponse }); }, error(error) { - logInspectorRequest().error({ json: error }); + logInspectorRequest().error({ + json: 'attributes' in error ? error.attributes : { message: error.message }, + }); }, }) ); diff --git a/src/plugins/data/common/search/expressions/essql.ts b/src/plugins/data/common/search/expressions/essql.ts index a5db4674a7d14..d943b406ff7f5 100644 --- a/src/plugins/data/common/search/expressions/essql.ts +++ b/src/plugins/data/common/search/expressions/essql.ts @@ -248,7 +248,9 @@ export const getEssqlFn = ({ getStartDependencies }: EssqlFnArguments) => { .ok({ json: rawResponse }); }, error(error) { - logInspectorRequest().error({ json: error }); + logInspectorRequest().error({ + json: 'attributes' in error ? error.attributes : { message: error.message }, + }); }, }) ); diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index b42cb7fdf4f25..4e5782a7468dd 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -458,7 +458,9 @@ export class SearchSource { const last$ = s$ .pipe( catchError((e) => { - requestResponder?.error({ json: e }); + requestResponder?.error({ + json: 'attributes' in e ? e.attributes : { message: e.message }, + }); return EMPTY; }), last(undefined, null), diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index e51bb8f208326..bc61d22c5a112 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -167,7 +167,6 @@ export type { SerializedSearchSourceFields, // errors IEsError, - Reason, WaitUntilNextSessionCompletesOptions, SearchResponseWarning, SearchResponseIncompleteWarning, diff --git a/src/plugins/data/public/search/errors/es_error.test.tsx b/src/plugins/data/public/search/errors/es_error.test.tsx index 4d1bc8b03b8f2..1b7f57d24d4f9 100644 --- a/src/plugins/data/public/search/errors/es_error.test.tsx +++ b/src/plugins/data/public/search/errors/es_error.test.tsx @@ -7,6 +7,7 @@ */ import { EsError } from './es_error'; +import { IEsError } from './types'; describe('EsError', () => { it('contains the same body as the wrapped error', () => { @@ -19,7 +20,7 @@ describe('EsError', () => { reason: 'top-level reason', }, }, - } as any; + } as IEsError; const esError = new EsError(error); expect(typeof esError.attributes).toEqual('object'); @@ -33,20 +34,22 @@ describe('EsError', () => { 'x_content_parse_exception: [x_content_parse_exception] Reason: [1:78] [date_histogram] failed to parse field [calendar_interval]', statusCode: 400, attributes: { - root_cause: [ - { - type: 'x_content_parse_exception', - reason: '[1:78] [date_histogram] failed to parse field [calendar_interval]', + error: { + root_cause: [ + { + type: 'x_content_parse_exception', + reason: '[1:78] [date_histogram] failed to parse field [calendar_interval]', + }, + ], + type: 'x_content_parse_exception', + reason: '[1:78] [date_histogram] failed to parse field [calendar_interval]', + caused_by: { + type: 'illegal_argument_exception', + reason: 'The supplied interval [2q] could not be parsed as a calendar interval.', }, - ], - type: 'x_content_parse_exception', - reason: '[1:78] [date_histogram] failed to parse field [calendar_interval]', - caused_by: { - type: 'illegal_argument_exception', - reason: 'The supplied interval [2q] could not be parsed as a calendar interval.', }, }, - } as any; + } as IEsError; const esError = new EsError(error); expect(esError.message).toEqual( 'EsError: The supplied interval [2q] could not be parsed as a calendar interval.' diff --git a/src/plugins/data/public/search/errors/es_error.tsx b/src/plugins/data/public/search/errors/es_error.tsx index a8d73baaf4d71..34d074a427187 100644 --- a/src/plugins/data/public/search/errors/es_error.tsx +++ b/src/plugins/data/public/search/errors/es_error.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { EuiCodeBlock, EuiSpacer } from '@elastic/eui'; -import { ApplicationStart } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; +import { ApplicationStart } from '@kbn/core/public'; import { KbnError } from '@kbn/kibana-utils-plugin/common'; import { IEsError } from './types'; import { getRootCause } from './utils'; @@ -20,7 +20,7 @@ export class EsError extends KbnError { constructor(protected readonly err: IEsError) { super( `EsError: ${ - getRootCause(err)?.reason || + getRootCause(err?.attributes?.error)?.reason || i18n.translate('data.esError.unknownRootCause', { defaultMessage: 'unknown' }) }` ); @@ -28,18 +28,20 @@ export class EsError extends KbnError { } public getErrorMessage(application: ApplicationStart) { - const rootCause = getRootCause(this.err)?.reason; - const topLevelCause = this.attributes?.reason; + if (!this.attributes?.error) { + return null; + } + + const rootCause = getRootCause(this.attributes.error)?.reason; + const topLevelCause = this.attributes.error.reason; const cause = rootCause ?? topLevelCause; return ( <> - {cause ? ( - - {cause} - - ) : null} + + {cause} + ); } diff --git a/src/plugins/data/public/search/errors/painless_error.test.tsx b/src/plugins/data/public/search/errors/painless_error.test.tsx index c4a540f7d21ab..4bf79a6f5b5d9 100644 --- a/src/plugins/data/public/search/errors/painless_error.test.tsx +++ b/src/plugins/data/public/search/errors/painless_error.test.tsx @@ -23,11 +23,13 @@ describe('PainlessError', () => { const e = new PainlessError({ statusCode: 400, message: 'search_phase_execution_exception', - attributes: searchPhaseException.error, + attributes: { + error: searchPhaseException.error, + }, }); const component = mount(e.getErrorMessage(startMock.application)); - const failedShards = e.attributes?.failed_shards![0]; + const failedShards = searchPhaseException.error.failed_shards![0]; const stackTraceElem = findTestSubject(component, 'painlessStackTrace').getDOMNode(); const stackTrace = failedShards!.reason.script_stack!.splice(-2).join('\n'); diff --git a/src/plugins/data/public/search/errors/painless_error.tsx b/src/plugins/data/public/search/errors/painless_error.tsx index 64f6c586932af..0435256f595cf 100644 --- a/src/plugins/data/public/search/errors/painless_error.tsx +++ b/src/plugins/data/public/search/errors/painless_error.tsx @@ -31,7 +31,7 @@ export class PainlessError extends EsError { }); } - const rootCause = getRootCause(this.err); + const rootCause = getRootCause(this.err.attributes?.error); const scriptFromStackTrace = rootCause?.script_stack ? rootCause?.script_stack?.slice(-2).join('\n') : undefined; @@ -78,7 +78,7 @@ export class PainlessError extends EsError { export function isPainlessError(err: Error | IEsError) { if (!isEsError(err)) return false; - const rootCause = getRootCause(err as IEsError); + const rootCause = getRootCause((err as IEsError).attributes?.error); if (!rootCause) return false; const { lang } = rootCause; diff --git a/src/plugins/data/public/search/errors/types.ts b/src/plugins/data/public/search/errors/types.ts index b89d784731c94..de03350a6d41c 100644 --- a/src/plugins/data/public/search/errors/types.ts +++ b/src/plugins/data/public/search/errors/types.ts @@ -5,39 +5,13 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { KibanaServerError } from '@kbn/kibana-utils-plugin/common'; - -export interface FailedShard { - shard: number; - index: string; - node: string; - reason: Reason; -} -export interface Reason { - type: string; - reason?: string; - script_stack?: string[]; - position?: { - offset: number; - start: number; - end: number; - }; - lang?: estypes.ScriptLanguage; - script?: string; - caused_by?: { - type: string; - reason: string; - }; -} +import { estypes } from '@elastic/elasticsearch'; +import { KibanaServerError } from '@kbn/kibana-utils-plugin/common'; interface IEsErrorAttributes { - type: string; - reason: string; - root_cause?: Reason[]; - failed_shards?: FailedShard[]; - caused_by?: IEsErrorAttributes; + rawResponse?: estypes.SearchResponseBody; + error?: estypes.ErrorCause; } export type IEsError = KibanaServerError; diff --git a/src/plugins/data/public/search/errors/utils.ts b/src/plugins/data/public/search/errors/utils.ts index f90a1fb461804..e1e2c51f87f3c 100644 --- a/src/plugins/data/public/search/errors/utils.ts +++ b/src/plugins/data/public/search/errors/utils.ts @@ -5,26 +5,21 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import type { ErrorCause } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { KibanaServerError } from '@kbn/kibana-utils-plugin/common'; -import type { FailedShard, Reason } from './types'; -export function getFailedShards(err: KibanaServerError): FailedShard | undefined { - const errorInfo = err.attributes; - const failedShards = errorInfo?.failed_shards || errorInfo?.caused_by?.failed_shards; - return failedShards ? failedShards[0] : undefined; +import { estypes } from '@elastic/elasticsearch'; + +function getFailedShardCause(error: estypes.ErrorCause): estypes.ErrorCause | undefined { + const failedShards = error.failed_shards || error.caused_by?.failed_shards; + return failedShards ? failedShards[0]?.reason : undefined; } -function getNestedCause(err: KibanaServerError | ErrorCause): Reason { - const attr = ((err as KibanaServerError).attributes || err) as ErrorCause; - const { type, reason, caused_by: causedBy } = attr; - if (causedBy) { - return getNestedCause(causedBy); - } - return { type, reason }; +function getNestedCause(error: estypes.ErrorCause): estypes.ErrorCause { + return error.caused_by ? getNestedCause(error.caused_by) : error; } -export function getRootCause(err: KibanaServerError) { - // Give shard failures priority, then try to get the error navigating nested objects - return getFailedShards(err)?.reason || getNestedCause(err); +export function getRootCause(error?: estypes.ErrorCause): estypes.ErrorCause | undefined { + return error + ? // Give shard failures priority, then try to get the error navigating nested objects + getFailedShardCause(error) || getNestedCause(error) + : undefined; } diff --git a/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts index 74b4a6cda7530..013afb428931d 100644 --- a/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts +++ b/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts @@ -22,6 +22,7 @@ import * as resourceNotFoundException from '../../../common/search/test_data/res import { BehaviorSubject } from 'rxjs'; import { dataPluginMock } from '../../mocks'; import { UI_SETTINGS } from '../../../common'; +import type { IEsError } from '../errors'; jest.mock('./utils', () => { const originalModule = jest.requireActual('./utils'); @@ -151,7 +152,9 @@ describe('SearchInterceptor', () => { new PainlessError({ statusCode: 400, message: 'search_phase_execution_exception', - attributes: searchPhaseException.error, + attributes: { + error: searchPhaseException.error, + }, }) ); expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(1); @@ -1452,10 +1455,12 @@ describe('SearchInterceptor', () => { }); test('Should throw Painless error on server error with OSS format', async () => { - const mockResponse: any = { + const mockResponse: IEsError = { statusCode: 400, message: 'search_phase_execution_exception', - attributes: searchPhaseException.error, + attributes: { + error: searchPhaseException.error, + }, }; fetchMock.mockRejectedValueOnce(mockResponse); const mockRequest: IEsSearchRequest = { @@ -1466,10 +1471,12 @@ describe('SearchInterceptor', () => { }); test('Should throw ES error on ES server error', async () => { - const mockResponse: any = { + const mockResponse: IEsError = { statusCode: 400, message: 'resource_not_found_exception', - attributes: resourceNotFoundException.error, + attributes: { + error: resourceNotFoundException.error, + }, }; fetchMock.mockRejectedValueOnce(mockResponse); const mockRequest: IEsSearchRequest = { diff --git a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts index 87f2ffe97c034..24b2e1c41216a 100644 --- a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts @@ -194,18 +194,18 @@ export class SearchInterceptor { // The timeout error is shown any time a request times out, or once per session, if the request is part of a session. this.showTimeoutError(err, options?.sessionId); return err; - } else if (e instanceof AbortError || e instanceof BfetchRequestError) { + } + + if (e instanceof AbortError || e instanceof BfetchRequestError) { // In the case an application initiated abort, throw the existing AbortError, same with BfetchRequestErrors return e; - } else if (isEsError(e)) { - if (isPainlessError(e)) { - return new PainlessError(e, options?.indexPattern); - } else { - return new EsError(e); - } - } else { - return e instanceof Error ? e : new Error(e.message); } + + if (isEsError(e)) { + return isPainlessError(e) ? new PainlessError(e, options?.indexPattern) : new EsError(e); + } + + return e instanceof Error ? e : new Error(e.message); } private getSerializableOptions(options?: ISearchOptions) { diff --git a/src/plugins/data/server/search/report_search_error.ts b/src/plugins/data/server/search/report_search_error.ts new file mode 100644 index 0000000000000..dc6bf2399abf6 --- /dev/null +++ b/src/plugins/data/server/search/report_search_error.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { errors } from '@elastic/elasticsearch'; +import { KibanaResponseFactory } from '@kbn/core/server'; +import { KbnError } from '@kbn/kibana-utils-plugin/common'; + +// Why not use just use kibana-utils-plugin KbnServerError and reportServerError? +// +// Search errors need to surface additional information +// such as rawResponse and sanitized requestParams. +// KbnServerError and reportServerError are used widely throughtout Kibana. +// KbnSearchError and reportSearchError exist to avoid polluting +// non-search usages of KbnServerError and reportServerError with extra information. +export class KbnSearchError extends KbnError { + public errBody?: Record; + constructor(message: string, public readonly statusCode: number, errBody?: Record) { + super(message); + this.errBody = errBody; + } +} + +/** + * Formats any error thrown into a standardized `KbnSearchError`. + * @param e `Error` or `ElasticsearchClientError` + * @returns `KbnSearchError` + */ +export function getKbnSearchError(e: Error) { + if (e instanceof KbnSearchError) return e; + return new KbnSearchError( + e.message ?? 'Unknown error', + e instanceof errors.ResponseError ? e.statusCode! : 500, + e instanceof errors.ResponseError ? e.body : undefined + ); +} + +/** + * + * @param res Formats a `KbnSearchError` into a server error response + * @param err + */ +export function reportSearchError(res: KibanaResponseFactory, err: KbnSearchError) { + return res.customError({ + statusCode: err.statusCode ?? 500, + body: { + message: err.message, + attributes: err.errBody + ? { + error: err.errBody.error, + rawResponse: err.errBody.response, + } + : undefined, + }, + }); +} diff --git a/src/plugins/data/server/search/routes/bsearch.ts b/src/plugins/data/server/search/routes/bsearch.ts index 95b094a3793cc..bf1aa4aaa3cbc 100644 --- a/src/plugins/data/server/search/routes/bsearch.ts +++ b/src/plugins/data/server/search/routes/bsearch.ts @@ -48,7 +48,12 @@ export function registerBsearchRoute( throw { message: err.message, statusCode: err.statusCode, - attributes: err.errBody?.error, + attributes: err.errBody + ? { + error: err.errBody.error, + rawResponse: err.errBody.response, + } + : undefined, }; }) ) diff --git a/src/plugins/data/server/search/routes/search.test.ts b/src/plugins/data/server/search/routes/search.test.ts index 10b7755b2c30f..26fad8b6890e2 100644 --- a/src/plugins/data/server/search/routes/search.test.ts +++ b/src/plugins/data/server/search/routes/search.test.ts @@ -14,13 +14,13 @@ import { registerSearchRoute } from './search'; import { DataPluginStart } from '../../plugin'; import * as searchPhaseException from '../../../common/search/test_data/search_phase_execution_exception.json'; import * as indexNotFoundException from '../../../common/search/test_data/index_not_found_exception.json'; -import { KbnServerError } from '@kbn/kibana-utils-plugin/server'; +import { KbnSearchError } from '../report_search_error'; describe('Search service', () => { let mockCoreSetup: MockedKeys>; - function mockEsError(message: string, statusCode: number, attributes?: Record) { - return new KbnServerError(message, statusCode, attributes); + function mockEsError(message: string, statusCode: number, errBody?: Record) { + return new KbnSearchError(message, statusCode, errBody); } async function runMockSearch(mockContext: any, mockRequest: any, mockResponse: any) { @@ -112,7 +112,10 @@ describe('Search service', () => { const error: any = mockResponse.customError.mock.calls[0][0]; expect(error.statusCode).toBe(400); expect(error.body.message).toBe('search_phase_execution_exception'); - expect(error.body.attributes).toBe(searchPhaseException.error); + expect(error.body.attributes).toEqual({ + error: searchPhaseException.error, + rawResponse: undefined, + }); }); it('handler returns an error response if the search throws an index not found error', async () => { @@ -138,7 +141,10 @@ describe('Search service', () => { const error: any = mockResponse.customError.mock.calls[0][0]; expect(error.statusCode).toBe(404); expect(error.body.message).toBe('index_not_found_exception'); - expect(error.body.attributes).toBe(indexNotFoundException.error); + expect(error.body.attributes).toEqual({ + error: indexNotFoundException.error, + rawResponse: undefined, + }); }); it('handler returns an error response if the search throws a general error', async () => { diff --git a/src/plugins/data/server/search/routes/search.ts b/src/plugins/data/server/search/routes/search.ts index 9d338095f25c0..8b302a81aea1a 100644 --- a/src/plugins/data/server/search/routes/search.ts +++ b/src/plugins/data/server/search/routes/search.ts @@ -9,6 +9,7 @@ import { first } from 'rxjs/operators'; import { schema } from '@kbn/config-schema'; import { reportServerError } from '@kbn/kibana-utils-plugin/server'; +import { reportSearchError } from '../report_search_error'; import { getRequestAbortedSignal } from '../../lib'; import type { DataPluginRouter } from '../types'; @@ -71,7 +72,7 @@ export function registerSearchRoute(router: DataPluginRouter): void { return res.ok({ body: response }); } catch (err) { - return reportServerError(res, err); + return reportSearchError(res, err); } } ); diff --git a/src/plugins/data/server/search/strategies/es_search/es_search_strategy.test.ts b/src/plugins/data/server/search/strategies/es_search/es_search_strategy.test.ts index 15a6a4df7eed8..9db6b00200bcc 100644 --- a/src/plugins/data/server/search/strategies/es_search/es_search_strategy.test.ts +++ b/src/plugins/data/server/search/strategies/es_search/es_search_strategy.test.ts @@ -14,7 +14,7 @@ import { SearchStrategyDependencies } from '../../types'; import * as indexNotFoundException from '../../../../common/search/test_data/index_not_found_exception.json'; import { errors } from '@elastic/elasticsearch'; -import { KbnServerError } from '@kbn/kibana-utils-plugin/server'; +import { KbnSearchError } from '../../report_search_error'; import { firstValueFrom } from 'rxjs'; describe('ES search strategy', () => { @@ -150,7 +150,7 @@ describe('ES search strategy', () => { .toPromise(); } catch (e) { expect(esClient.search).toBeCalled(); - expect(e).toBeInstanceOf(KbnServerError); + expect(e).toBeInstanceOf(KbnSearchError); expect(e.statusCode).toBe(404); expect(e.message).toBe(errResponse.message); expect(e.errBody).toBe(indexNotFoundException); @@ -167,7 +167,7 @@ describe('ES search strategy', () => { .toPromise(); } catch (e) { expect(esClient.search).toBeCalled(); - expect(e).toBeInstanceOf(KbnServerError); + expect(e).toBeInstanceOf(KbnSearchError); expect(e.statusCode).toBe(500); expect(e.message).toBe(errResponse.message); expect(e.errBody).toBe(undefined); @@ -184,14 +184,14 @@ describe('ES search strategy', () => { .toPromise(); } catch (e) { expect(esClient.search).toBeCalled(); - expect(e).toBeInstanceOf(KbnServerError); + expect(e).toBeInstanceOf(KbnSearchError); expect(e.statusCode).toBe(500); expect(e.message).toBe(errResponse.message); expect(e.errBody).toBe(undefined); } }); - it('throws KbnServerError for unknown index type', async () => { + it('throws KbnSearchError for unknown index type', async () => { const params = { index: 'logstash-*', ignore_unavailable: false, timeout: '1000ms' }; try { @@ -200,7 +200,7 @@ describe('ES search strategy', () => { .toPromise(); } catch (e) { expect(esClient.search).not.toBeCalled(); - expect(e).toBeInstanceOf(KbnServerError); + expect(e).toBeInstanceOf(KbnSearchError); expect(e.message).toBe('Unsupported index pattern type banana'); expect(e.statusCode).toBe(400); expect(e.errBody).toBe(undefined); diff --git a/src/plugins/data/server/search/strategies/es_search/es_search_strategy.ts b/src/plugins/data/server/search/strategies/es_search/es_search_strategy.ts index b2aed5804f248..64b2234a573c8 100644 --- a/src/plugins/data/server/search/strategies/es_search/es_search_strategy.ts +++ b/src/plugins/data/server/search/strategies/es_search/es_search_strategy.ts @@ -9,7 +9,7 @@ import { firstValueFrom, from, Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; import type { Logger, SharedGlobalConfig } from '@kbn/core/server'; -import { getKbnServerError, KbnServerError } from '@kbn/kibana-utils-plugin/server'; +import { getKbnSearchError, KbnSearchError } from '../../report_search_error'; import type { ISearchStrategy } from '../../types'; import type { SearchUsage } from '../../collectors/search'; import { getDefaultSearchParams, getShardTimeout } from './request_utils'; @@ -25,14 +25,14 @@ export const esSearchStrategyProvider = ( * @param request * @param options * @param deps - * @throws `KbnServerError` + * @throws `KbnSearchError` * @returns `Observable>` */ search: (request, { abortSignal, transport, ...options }, { esClient, uiSettingsClient }) => { // Only default index pattern type is supported here. // See ese for other type support. if (request.indexType) { - throw new KbnServerError(`Unsupported index pattern type ${request.indexType}`, 400); + throw new KbnSearchError(`Unsupported index pattern type ${request.indexType}`, 400); } const isPit = request.params?.body?.pit != null; @@ -57,7 +57,7 @@ export const esSearchStrategyProvider = ( const response = shimHitsTotal(body, options); return toKibanaSearchResponse(response); } catch (e) { - throw getKbnServerError(e); + throw getKbnSearchError(e); } }; diff --git a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.test.ts b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.test.ts index 96e401204978f..6c1746984c86b 100644 --- a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.test.ts +++ b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.test.ts @@ -8,6 +8,7 @@ import { BehaviorSubject, firstValueFrom } from 'rxjs'; import { KbnServerError } from '@kbn/kibana-utils-plugin/server'; +import { KbnSearchError } from '../../report_search_error'; import { errors } from '@elastic/elasticsearch'; import * as indexNotFoundException from '../../../../common/search/test_data/index_not_found_exception.json'; import * as xContentParseException from '../../../../common/search/test_data/x_content_parse_exception.json'; @@ -456,14 +457,14 @@ describe('ES search strategy', () => { mockLogger ); - let err: KbnServerError | undefined; + let err: KbnSearchError | undefined; try { await esSearch.search({ params }, {}, mockDeps).toPromise(); } catch (e) { err = e; } expect(mockSubmitCaller).toBeCalled(); - expect(err).toBeInstanceOf(KbnServerError); + expect(err).toBeInstanceOf(KbnSearchError); expect(err?.statusCode).toBe(404); expect(err?.message).toBe(errResponse.message); expect(err?.errBody).toBe(indexNotFoundException); @@ -481,14 +482,14 @@ describe('ES search strategy', () => { mockLogger ); - let err: KbnServerError | undefined; + let err: KbnSearchError | undefined; try { await esSearch.search({ params }, {}, mockDeps).toPromise(); } catch (e) { err = e; } expect(mockSubmitCaller).toBeCalled(); - expect(err).toBeInstanceOf(KbnServerError); + expect(err).toBeInstanceOf(KbnSearchError); expect(err?.statusCode).toBe(500); expect(err?.message).toBe(errResponse.message); expect(err?.errBody).toBe(undefined); diff --git a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts index 88d1606935562..460d8f95ed3e4 100644 --- a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts +++ b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts @@ -11,7 +11,8 @@ import type { IScopedClusterClient, Logger, SharedGlobalConfig } from '@kbn/core import { catchError, tap } from 'rxjs/operators'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { firstValueFrom, from } from 'rxjs'; -import { getKbnServerError, KbnServerError } from '@kbn/kibana-utils-plugin/server'; +import { getKbnServerError } from '@kbn/kibana-utils-plugin/server'; +import { getKbnSearchError, KbnSearchError } from '../../report_search_error'; import type { ISearchStrategy, SearchStrategyDependencies } from '../../types'; import type { IAsyncSearchOptions, @@ -94,7 +95,7 @@ export const enhancedEsSearchStrategyProvider = ( tap((response) => (id = response.id)), tap(searchUsageObserver(logger, usage)), catchError((e) => { - throw getKbnServerError(e); + throw getKbnSearchError(e); }) ); } @@ -136,7 +137,7 @@ export const enhancedEsSearchStrategyProvider = ( ...getTotalLoaded(response), }; } catch (e) { - throw getKbnServerError(e); + throw getKbnSearchError(e); } } @@ -146,12 +147,12 @@ export const enhancedEsSearchStrategyProvider = ( * @param options * @param deps `SearchStrategyDependencies` * @returns `Observable>` - * @throws `KbnServerError` + * @throws `KbnSearchError` */ search: (request, options: IAsyncSearchOptions, deps) => { logger.debug(`search ${JSON.stringify(request.params) || request.id}`); if (request.indexType && request.indexType !== 'rollup') { - throw new KbnServerError('Unknown indexType', 400); + throw new KbnSearchError('Unknown indexType', 400); } if (request.indexType === undefined || !deps.rollupsEnabled) { diff --git a/src/plugins/data/server/search/strategies/esql_search/esql_search_strategy.ts b/src/plugins/data/server/search/strategies/esql_search/esql_search_strategy.ts index 2af032826189f..460755a74df8f 100644 --- a/src/plugins/data/server/search/strategies/esql_search/esql_search_strategy.ts +++ b/src/plugins/data/server/search/strategies/esql_search/esql_search_strategy.ts @@ -8,7 +8,7 @@ import { from } from 'rxjs'; import type { Logger } from '@kbn/core/server'; -import { getKbnServerError, KbnServerError } from '@kbn/kibana-utils-plugin/server'; +import { getKbnSearchError, KbnSearchError } from '../../report_search_error'; import type { ISearchStrategy } from '../../types'; const ES_TIMEOUT_IN_MS = 120000; @@ -21,7 +21,7 @@ export const esqlSearchStrategyProvider = ( * @param request * @param options * @param deps - * @throws `KbnServerError` + * @throws `KbnSearchError` * @returns `Observable>` */ search: (request, { abortSignal, ...options }, { esClient, uiSettingsClient }) => { @@ -39,7 +39,7 @@ export const esqlSearchStrategyProvider = ( // Only default index pattern type is supported here. // See ese for other type support. if (request.indexType) { - throw new KbnServerError(`Unsupported index pattern type ${request.indexType}`, 400); + throw new KbnSearchError(`Unsupported index pattern type ${request.indexType}`, 400); } const search = async () => { @@ -67,7 +67,7 @@ export const esqlSearchStrategyProvider = ( warning: headers?.warning, }; } catch (e) { - throw getKbnServerError(e); + throw getKbnSearchError(e); } }; diff --git a/src/plugins/data/server/search/strategies/sql_search/sql_search_strategy.test.ts b/src/plugins/data/server/search/strategies/sql_search/sql_search_strategy.test.ts index 530ee16ae75b9..700c658de10c0 100644 --- a/src/plugins/data/server/search/strategies/sql_search/sql_search_strategy.test.ts +++ b/src/plugins/data/server/search/strategies/sql_search/sql_search_strategy.test.ts @@ -7,7 +7,7 @@ */ import { merge } from 'lodash'; -import { KbnServerError } from '@kbn/kibana-utils-plugin/server'; +import { KbnSearchError } from '../../report_search_error'; import { errors } from '@elastic/elasticsearch'; import * as indexNotFoundException from '../../../../common/search/test_data/index_not_found_exception.json'; import { SearchStrategyDependencies } from '../../types'; @@ -221,14 +221,14 @@ describe('SQL search strategy', () => { }; const esSearch = await sqlSearchStrategyProvider(mockSearchConfig, mockLogger); - let err: KbnServerError | undefined; + let err: KbnSearchError | undefined; try { await esSearch.search({ params }, {}, mockDeps).toPromise(); } catch (e) { err = e; } expect(mockSqlQuery).toBeCalled(); - expect(err).toBeInstanceOf(KbnServerError); + expect(err).toBeInstanceOf(KbnSearchError); expect(err?.statusCode).toBe(404); expect(err?.message).toBe(errResponse.message); expect(err?.errBody).toBe(indexNotFoundException); @@ -245,14 +245,14 @@ describe('SQL search strategy', () => { }; const esSearch = await sqlSearchStrategyProvider(mockSearchConfig, mockLogger); - let err: KbnServerError | undefined; + let err: KbnSearchError | undefined; try { await esSearch.search({ params }, {}, mockDeps).toPromise(); } catch (e) { err = e; } expect(mockSqlQuery).toBeCalled(); - expect(err).toBeInstanceOf(KbnServerError); + expect(err).toBeInstanceOf(KbnSearchError); expect(err?.statusCode).toBe(500); expect(err?.message).toBe(errResponse.message); expect(err?.errBody).toBe(undefined); diff --git a/src/plugins/data/server/search/strategies/sql_search/sql_search_strategy.ts b/src/plugins/data/server/search/strategies/sql_search/sql_search_strategy.ts index d5d1eafc5c214..34134a1491cd0 100644 --- a/src/plugins/data/server/search/strategies/sql_search/sql_search_strategy.ts +++ b/src/plugins/data/server/search/strategies/sql_search/sql_search_strategy.ts @@ -11,6 +11,7 @@ import type { IScopedClusterClient, Logger } from '@kbn/core/server'; import { catchError, tap } from 'rxjs/operators'; import { SqlQueryResponse } from '@elastic/elasticsearch/lib/api/types'; import { getKbnServerError } from '@kbn/kibana-utils-plugin/server'; +import { getKbnSearchError } from '../../report_search_error'; import type { ISearchStrategy, SearchStrategyDependencies } from '../../types'; import type { IAsyncSearchOptions, @@ -94,7 +95,7 @@ export const sqlSearchStrategyProvider = ( }).pipe( tap((response) => (id = response.id)), catchError((e) => { - throw getKbnServerError(e); + throw getKbnSearchError(e); }) ); } @@ -105,7 +106,7 @@ export const sqlSearchStrategyProvider = ( * @param options * @param deps `SearchStrategyDependencies` * @returns `Observable>` - * @throws `KbnServerError` + * @throws `KbnSearchError` */ search: (request, options: IAsyncSearchOptions, deps) => { logger.debug(`sql search: search request=${JSON.stringify(request)}`); diff --git a/test/api_integration/apis/search/verify_error.ts b/test/api_integration/apis/search/verify_error.ts index 1973fe4e4ab36..b7a38f1117fe2 100644 --- a/test/api_integration/apis/search/verify_error.ts +++ b/test/api_integration/apis/search/verify_error.ts @@ -20,7 +20,8 @@ export const verifyErrorResponse = ( } if (shouldHaveAttrs) { expect(r).to.have.property('attributes'); - expect(r.attributes).to.have.property('root_cause'); + expect(r.attributes).to.have.property('error'); + expect(r.attributes.error).to.have.property('root_cause'); } else { expect(r).not.to.have.property('attributes'); } diff --git a/x-pack/plugins/lens/public/editor_frame_service/error_helper.test.ts b/x-pack/plugins/lens/public/editor_frame_service/error_helper.test.ts index fe5c1f85f1e92..7e3b696a8e75f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/error_helper.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/error_helper.test.ts @@ -21,6 +21,45 @@ const runtimeFieldError = { message: 'status_exception', statusCode: 400, attributes: { + error: { + type: 'status_exception', + reason: 'error while executing search', + caused_by: { + type: 'search_phase_execution_exception', + reason: 'all shards failed', + phase: 'query', + grouped: true, + failed_shards: [ + { + shard: 0, + index: 'indexpattern_source', + node: 'jtqB1-UhQluyjeXIpQFqAA', + reason: { + type: 'script_exception', + reason: 'runtime error', + script_stack: [ + 'java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:68)', + 'java.base/java.lang.Integer.parseInt(Integer.java:652)', + 'java.base/java.lang.Integer.parseInt(Integer.java:770)', + "emit(Integer.parseInt('hello'))", + ' ^---- HERE', + ], + script: "emit(Integer.parseInt('hello'))", + lang: 'painless', + position: { offset: 12, start: 0, end: 31 }, + caused_by: { + type: 'number_format_exception', + reason: 'For input string: "hello"', + }, + }, + }, + ], + }, + }, + }, + }, + attributes: { + error: { type: 'status_exception', reason: 'error while executing search', caused_by: { @@ -53,38 +92,6 @@ const runtimeFieldError = { }, }, }, - attributes: { - type: 'status_exception', - reason: 'error while executing search', - caused_by: { - type: 'search_phase_execution_exception', - reason: 'all shards failed', - phase: 'query', - grouped: true, - failed_shards: [ - { - shard: 0, - index: 'indexpattern_source', - node: 'jtqB1-UhQluyjeXIpQFqAA', - reason: { - type: 'script_exception', - reason: 'runtime error', - script_stack: [ - 'java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:68)', - 'java.base/java.lang.Integer.parseInt(Integer.java:652)', - 'java.base/java.lang.Integer.parseInt(Integer.java:770)', - "emit(Integer.parseInt('hello'))", - ' ^---- HERE', - ], - script: "emit(Integer.parseInt('hello'))", - lang: 'painless', - position: { offset: 12, start: 0, end: 31 }, - caused_by: { type: 'number_format_exception', reason: 'For input string: "hello"' }, - }, - }, - ], - }, - }, }, }; @@ -99,6 +106,31 @@ const scriptedFieldError = { message: 'status_exception', statusCode: 500, attributes: { + error: { + type: 'status_exception', + reason: 'error while executing search', + caused_by: { + type: 'search_phase_execution_exception', + reason: 'all shards failed', + phase: 'query', + grouped: true, + failed_shards: [ + { + shard: 0, + index: 'indexpattern_source', + node: 'jtqB1-UhQluyjeXIpQFqAA', + reason: { + type: 'aggregation_execution_exception', + reason: 'Unsupported script value [hello], expected a number, date, or boolean', + }, + }, + ], + }, + }, + }, + }, + attributes: { + error: { type: 'status_exception', reason: 'error while executing search', caused_by: { @@ -120,27 +152,6 @@ const scriptedFieldError = { }, }, }, - attributes: { - type: 'status_exception', - reason: 'error while executing search', - caused_by: { - type: 'search_phase_execution_exception', - reason: 'all shards failed', - phase: 'query', - grouped: true, - failed_shards: [ - { - shard: 0, - index: 'indexpattern_source', - node: 'jtqB1-UhQluyjeXIpQFqAA', - reason: { - type: 'aggregation_execution_exception', - reason: 'Unsupported script value [hello], expected a number, date, or boolean', - }, - }, - ], - }, - }, }, }; @@ -174,41 +185,7 @@ const tsdbCounterUsedWithWrongOperationError = { name: 'Error', original: { attributes: { - type: 'status_exception', - reason: 'error while executing search', - caused_by: { - type: 'search_phase_execution_exception', - reason: 'all shards failed', - phase: 'query', - grouped: true, - failed_shards: [ - { - shard: 0, - index: 'tsdb_index', - reason: { - type: 'illegal_argument_exception', - reason: - 'Field [bytes_counter] of type [long][counter] is not supported for aggregation [sum]', - }, - }, - ], - caused_by: { - type: 'illegal_argument_exception', - reason: - 'Field [bytes_counter] of type [long][counter] is not supported for aggregation [sum]', - caused_by: { - type: 'illegal_argument_exception', - reason: - 'Field [bytes_counter] of type [long][counter] is not supported for aggregation [sum]', - }, - }, - }, - }, - err: { - message: - 'status_exception\n\tCaused by:\n\t\tsearch_phase_execution_exception: all shards failed', - statusCode: 400, - attributes: { + error: { type: 'status_exception', reason: 'error while executing search', caused_by: { @@ -240,6 +217,44 @@ const tsdbCounterUsedWithWrongOperationError = { }, }, }, + err: { + message: + 'status_exception\n\tCaused by:\n\t\tsearch_phase_execution_exception: all shards failed', + statusCode: 400, + attributes: { + error: { + type: 'status_exception', + reason: 'error while executing search', + caused_by: { + type: 'search_phase_execution_exception', + reason: 'all shards failed', + phase: 'query', + grouped: true, + failed_shards: [ + { + shard: 0, + index: 'tsdb_index', + reason: { + type: 'illegal_argument_exception', + reason: + 'Field [bytes_counter] of type [long][counter] is not supported for aggregation [sum]', + }, + }, + ], + caused_by: { + type: 'illegal_argument_exception', + reason: + 'Field [bytes_counter] of type [long][counter] is not supported for aggregation [sum]', + caused_by: { + type: 'illegal_argument_exception', + reason: + 'Field [bytes_counter] of type [long][counter] is not supported for aggregation [sum]', + }, + }, + }, + }, + }, + }, }, }; diff --git a/x-pack/plugins/lens/public/editor_frame_service/error_helper.tsx b/x-pack/plugins/lens/public/editor_frame_service/error_helper.tsx index fd98a0654983e..c336101033eae 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/error_helper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/error_helper.tsx @@ -7,18 +7,16 @@ import { i18n } from '@kbn/i18n'; import { isEqual, uniqWith } from 'lodash'; +import { estypes } from '@elastic/elasticsearch'; import { ExpressionRenderError } from '@kbn/expressions-plugin/public'; import type { CoreStart } from '@kbn/core/public'; import { isEsError } from '@kbn/data-plugin/public'; -import type { IEsError, Reason } from '@kbn/data-plugin/public'; import React from 'react'; import { EuiLink } from '@elastic/eui'; import { RemovableUserMessage } from '../types'; -type ErrorCause = Required['attributes']; - interface RequestError extends Error { - body?: { attributes?: { error: { caused_by: ErrorCause } } }; + body?: { attributes?: { error: { caused_by: estypes.ErrorCause } } }; } interface ReasonDescription { @@ -62,7 +60,7 @@ function getNestedErrorClauseWithContext({ caused_by: causedBy, lang, script, -}: Reason): ReasonDescription[] { +}: estypes.ErrorCause): ReasonDescription[] { if (!causedBy) { // scripted fields error has changed with no particular hint about painless in it, // so it tries to lookup in the message for the script word @@ -83,12 +81,12 @@ function getNestedErrorClauseWithContext({ return [{ ...payload, context: { type, reason } }]; } -function getNestedErrorClause(e: ErrorCause | Reason): ReasonDescription[] { +function getNestedErrorClause(e: estypes.ErrorCause): ReasonDescription[] { const { type, reason = '', caused_by: causedBy } = e; // Painless scripts errors are nested within the failed_shards property if ('failed_shards' in e) { if (e.failed_shards) { - return e.failed_shards.flatMap((shardCause) => + return (e.failed_shards as estypes.ShardFailure[]).flatMap((shardCause) => getNestedErrorClauseWithContext(shardCause.reason) ); } @@ -101,13 +99,15 @@ function getNestedErrorClause(e: ErrorCause | Reason): ReasonDescription[] { function getErrorSources(e: Error) { if (isRequestError(e)) { - return getNestedErrorClause(e.body!.attributes!.error as ErrorCause); + return getNestedErrorClause(e.body!.attributes!.error as estypes.ErrorCause); } if (isEsError(e)) { - if (e.attributes?.reason) { - return getNestedErrorClause(e.attributes); + if (e.attributes?.error?.reason) { + return getNestedErrorClause(e.attributes.error); + } + if (e.attributes?.error?.caused_by) { + return getNestedErrorClause(e.attributes.error.caused_by); } - return getNestedErrorClause(e.attributes?.caused_by as ErrorCause); } return []; } diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts index 250c000575d07..ad3334e536793 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts @@ -234,7 +234,7 @@ describe('useAppToasts', () => { it('prefers the attributes reason if we have it for the message', async () => { const error: IEsError = { - attributes: { type: 'some type', reason: 'message we want' }, + attributes: { error: { type: 'some type', reason: 'message we want' } }, statusCode: 200, message: 'message we do not want', }; @@ -244,11 +244,11 @@ describe('useAppToasts', () => { it('works with an EsError, by using the inner error and not outer error if available', async () => { const error: MaybeESError = { - attributes: { type: 'some type', reason: 'message we want' }, + attributes: { error: { type: 'some type', reason: 'message we want' } }, statusCode: 400, err: { statusCode: 200, - attributes: { reason: 'attribute message we do not want' }, + attributes: { error: { reason: 'attribute message we do not want' } }, }, message: 'main message we do not want', }; @@ -258,11 +258,11 @@ describe('useAppToasts', () => { it('creates a stack trace of a EsError and not the outer object', async () => { const error: MaybeESError = { - attributes: { type: 'some type', reason: 'message we do not want' }, + attributes: { error: { type: 'some type', reason: 'message we do not want' } }, statusCode: 400, err: { statusCode: 200, - attributes: { reason: 'attribute message we do want' }, + attributes: { error: { reason: 'attribute message we do want' } }, }, message: 'main message we do not want', }; @@ -270,7 +270,7 @@ describe('useAppToasts', () => { const parsedStack = JSON.parse(result.stack ?? ''); expect(parsedStack).toEqual({ statusCode: 200, - attributes: { reason: 'attribute message we do want' }, + attributes: { error: { reason: 'attribute message we do want' } }, }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts index 3c4ad68b221ea..99500a42b8c35 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts @@ -98,8 +98,10 @@ export const esErrorToErrorStack = (error: IEsError & MaybeESError): Error => { ? `(${error.statusCode})` : ''; const stringifiedError = getStringifiedStack(maybeUnWrapped); - const adaptedError = new Error(`${error.attributes?.reason ?? error.message} ${statusCode}`); - adaptedError.name = error.attributes?.reason ?? error.message; + const adaptedError = new Error( + `${error.attributes?.error?.reason ?? error.message} ${statusCode}` + ); + adaptedError.name = error.attributes?.error?.reason ?? error.message; if (stringifiedError != null) { adaptedError.stack = stringifiedError; } diff --git a/x-pack/plugins/timelines/public/hooks/use_app_toasts.ts b/x-pack/plugins/timelines/public/hooks/use_app_toasts.ts index 4a3cc315ae693..a33ee86e0a31a 100644 --- a/x-pack/plugins/timelines/public/hooks/use_app_toasts.ts +++ b/x-pack/plugins/timelines/public/hooks/use_app_toasts.ts @@ -93,8 +93,10 @@ export const esErrorToErrorStack = (error: IEsError & MaybeESError): Error => { ? `(${error.statusCode})` : ''; const stringifiedError = getStringifiedStack(maybeUnWrapped); - const adaptedError = new Error(`${error.attributes?.reason ?? error.message} ${statusCode}`); - adaptedError.name = error.attributes?.reason ?? error.message; + const adaptedError = new Error( + `${error.attributes?.error?.reason ?? error.message} ${statusCode}` + ); + adaptedError.name = error.attributes?.error?.reason ?? error.message; if (stringifiedError != null) { adaptedError.stack = stringifiedError; } diff --git a/x-pack/test/api_integration/apis/search/search.ts b/x-pack/test/api_integration/apis/search/search.ts index 48ff19e51623d..391923601d7c5 100644 --- a/x-pack/test/api_integration/apis/search/search.ts +++ b/x-pack/test/api_integration/apis/search/search.ts @@ -411,7 +411,11 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .send() .expect(400); - verifyErrorResponse(resp.body, 400, 'illegal_argument_exception', true); + + expect(resp.body.statusCode).to.be(400); + expect(resp.body.message).to.include.string('illegal_argument_exception'); + expect(resp.body).to.have.property('attributes'); + expect(resp.body.attributes).to.have.property('root_cause'); }); it('should delete an in-progress search', async function () { diff --git a/x-pack/test_serverless/api_integration/test_suites/common/search_oss/verify_error.ts b/x-pack/test_serverless/api_integration/test_suites/common/search_oss/verify_error.ts index 3523cefc8b33f..be1ceaf1d535e 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/search_oss/verify_error.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/search_oss/verify_error.ts @@ -19,7 +19,8 @@ export const verifyErrorResponse = ( } if (shouldHaveAttrs) { expect(r).to.have.property('attributes'); - expect(r.attributes).to.have.property('root_cause'); + expect(r.attributes).to.have.property('error'); + expect(r.attributes.error).to.have.property('root_cause'); } else { expect(r).not.to.have.property('attributes'); } diff --git a/x-pack/test_serverless/api_integration/test_suites/common/search_xpack/search.ts b/x-pack/test_serverless/api_integration/test_suites/common/search_xpack/search.ts index d820f22e72567..ecced2db57ee9 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/search_xpack/search.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/search_xpack/search.ts @@ -380,7 +380,10 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .send() .expect(400); - verifyErrorResponse(resp.body, 400, 'illegal_argument_exception', true); + expect(resp.body.statusCode).to.be(400); + expect(resp.body.message).to.include.string('illegal_argument_exception'); + expect(resp.body).to.have.property('attributes'); + expect(resp.body.attributes).to.have.property('root_cause'); }); it('should delete an in-progress search', async function () {