diff --git a/docs/development/core/server/kibana-plugin-server.callapioptions.md b/docs/development/core/server/kibana-plugin-server.callapioptions.md index c4955929c7519..ffdf638b236bb 100644 --- a/docs/development/core/server/kibana-plugin-server.callapioptions.md +++ b/docs/development/core/server/kibana-plugin-server.callapioptions.md @@ -16,5 +16,6 @@ export interface CallAPIOptions | Property | Type | Description | | --- | --- | --- | +| [signal](./kibana-plugin-server.callapioptions.signal.md) | <code>AbortSignal</code> | A signal object that allows you to abort the request via an AbortController object. | | [wrap401Errors](./kibana-plugin-server.callapioptions.wrap401errors.md) | <code>boolean</code> | Indicates whether <code>401 Unauthorized</code> errors returned from the Elasticsearch API should be wrapped into <code>Boom</code> error instances with properly set <code>WWW-Authenticate</code> header that could have been returned by the API itself. If API didn't specify that then <code>Basic realm="Authorization Required"</code> is used as <code>WWW-Authenticate</code>. | diff --git a/docs/development/core/server/kibana-plugin-server.callapioptions.signal.md b/docs/development/core/server/kibana-plugin-server.callapioptions.signal.md new file mode 100644 index 0000000000000..402ed0ca8e34c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.callapioptions.signal.md @@ -0,0 +1,13 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CallAPIOptions](./kibana-plugin-server.callapioptions.md) > [signal](./kibana-plugin-server.callapioptions.signal.md) + +## CallAPIOptions.signal property + +A signal object that allows you to abort the request via an AbortController object. + +<b>Signature:</b> + +```typescript +signal?: AbortSignal; +``` diff --git a/src/core/server/elasticsearch/cluster_client.test.ts b/src/core/server/elasticsearch/cluster_client.test.ts index 30a1ac7a038ed..de28818072bcf 100644 --- a/src/core/server/elasticsearch/cluster_client.test.ts +++ b/src/core/server/elasticsearch/cluster_client.test.ts @@ -165,6 +165,25 @@ describe('#callAsInternalUser', () => { ).rejects.toStrictEqual(mockAuthenticationError); }); + test('aborts the request and rejects if a signal is provided and aborted', async () => { + const controller = new AbortController(); + + // The ES client returns a promise with an additional `abort` method to abort the request + const mockValue: any = Promise.resolve(); + mockValue.abort = jest.fn(); + mockEsClientInstance.ping.mockReturnValue(mockValue); + + const promise = clusterClient.callAsInternalUser('ping', undefined, { + wrap401Errors: false, + signal: controller.signal, + }); + + controller.abort(); + + expect(mockValue.abort).toHaveBeenCalled(); + await expect(promise).rejects.toThrowErrorMatchingInlineSnapshot(`"Request was aborted"`); + }); + test('does not override WWW-Authenticate if returned by Elasticsearch', async () => { const mockAuthenticationError = new (errors.AuthenticationException as any)( 'Authentication Exception', diff --git a/src/core/server/elasticsearch/cluster_client.ts b/src/core/server/elasticsearch/cluster_client.ts index 917bb1b93e37f..45e3f0a20c0c4 100644 --- a/src/core/server/elasticsearch/cluster_client.ts +++ b/src/core/server/elasticsearch/cluster_client.ts @@ -42,6 +42,10 @@ export interface CallAPIOptions { * then `Basic realm="Authorization Required"` is used as `WWW-Authenticate`. */ wrap401Errors: boolean; + /** + * A signal object that allows you to abort the request via an AbortController object. + */ + signal?: AbortSignal; } /** @@ -57,7 +61,7 @@ async function callAPI( endpoint: string, clientParams: Record<string, unknown> = {}, options: CallAPIOptions = { wrap401Errors: true } -) { +): Promise<any> { const clientPath = endpoint.split('.'); const api: any = get(client, clientPath); if (!api) { @@ -66,7 +70,16 @@ async function callAPI( const apiContext = clientPath.length === 1 ? client : get(client, clientPath.slice(0, -1)); try { - return await api.call(apiContext, clientParams); + return await new Promise((resolve, reject) => { + const request = api.call(apiContext, clientParams); + if (options.signal) { + options.signal.addEventListener('abort', () => { + request.abort(); + reject(new Error('Request was aborted')); + }); + } + return request.then(resolve, reject); + }); } catch (err) { if (!options.wrap401Errors || err.statusCode !== 401) { throw err; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 88ebe1e54bdce..43712d63abb53 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -43,6 +43,7 @@ export function bootstrap({ configs, cliArgs, applyConfigOverrides, features, }: // @public export interface CallAPIOptions { + signal?: AbortSignal; wrap401Errors: boolean; } diff --git a/src/legacy/core_plugins/elasticsearch/index.d.ts b/src/legacy/core_plugins/elasticsearch/index.d.ts index 9a524ab06e7bf..b26964e92f257 100644 --- a/src/legacy/core_plugins/elasticsearch/index.d.ts +++ b/src/legacy/core_plugins/elasticsearch/index.d.ts @@ -206,6 +206,7 @@ export interface DeprecationAPIResponse { export interface CallClusterOptions { wrap401Errors?: boolean; + signal?: AbortSignal; } export interface CallClusterWithRequest {