From 3ca1dc1485a782097207909a2f003d658f9af7be Mon Sep 17 00:00:00 2001 From: Eli Perelman Date: Wed, 8 May 2019 11:37:15 -0500 Subject: [PATCH] Expose an HTTP-request browser client (#35486) * Expose an HTTP-request browser client * Fix failing tests from kfetch refactor * Make abort() non-enumerable, fix review issues * Move kfetch test setup to build-excluded location * Add ndjson tests to browser http service * Lint fixes * Fix missing update of del to delete in http mock * Fix problems with merging headers with undefined Content-Type * Delete correct property from updated options * Linting fix * Fix reference to kfetch_test_setup due to moving test file * Add tests and fix implementation of abortables * Add missing http start mock contract, fix test in CI * Remove abortable promise functionality * Fix DELETE method handler, remove unnecessary promise wrapper --- src/core/public/core_system.ts | 7 +- src/core/public/http/_import_objects.ndjson | 1 + src/core/public/http/fetch.ts | 91 ++++++ .../public/http/http_fetch_error.ts} | 33 +-- src/core/public/http/http_service.mock.ts | 36 +-- src/core/public/http/http_service.test.ts | 261 ++++++++++++++++-- src/core/public/http/http_service.ts | 25 +- src/core/public/http/index.ts | 1 + src/core/public/http/types.ts | 55 ++++ src/core/public/legacy/legacy_service.ts | 1 + .../components/query_bar.test.mocks.ts | 8 + .../error_auto_create_index.test.js | 24 +- .../ui/public/kfetch/_import_objects.ndjson | 1 + src/legacy/ui/public/kfetch/index.ts | 22 +- src/legacy/ui/public/kfetch/kfetch.test.ts | 102 ++++--- src/legacy/ui/public/kfetch/kfetch.ts | 93 +++---- .../ui/public/kfetch/kfetch_abortable.test.ts | 39 --- src/test_utils/public/kfetch_test_setup.ts | 38 +++ .../public/search/rollup_search_strategy.js | 23 +- 19 files changed, 626 insertions(+), 235 deletions(-) create mode 100644 src/core/public/http/_import_objects.ndjson create mode 100644 src/core/public/http/fetch.ts rename src/{legacy/ui/public/kfetch/kfetch_abortable.ts => core/public/http/http_fetch_error.ts} (57%) create mode 100644 src/core/public/http/types.ts create mode 100644 src/legacy/ui/public/kfetch/_import_objects.ndjson delete mode 100644 src/legacy/ui/public/kfetch/kfetch_abortable.test.ts create mode 100644 src/test_utils/public/kfetch_test_setup.ts diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index b6fb433f50476a9..3ec0e491e3d2c9e 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -118,9 +118,12 @@ export class CoreSystem { const i18n = this.i18n.setup(); const injectedMetadata = this.injectedMetadata.setup(); this.fatalErrorsSetup = this.fatalErrors.setup({ injectedMetadata, i18n }); - - const http = this.http.setup({ fatalErrors: this.fatalErrorsSetup }); const basePath = this.basePath.setup({ injectedMetadata }); + const http = this.http.setup({ + basePath, + injectedMetadata, + fatalErrors: this.fatalErrorsSetup, + }); const uiSettings = this.uiSettings.setup({ http, injectedMetadata, diff --git a/src/core/public/http/_import_objects.ndjson b/src/core/public/http/_import_objects.ndjson new file mode 100644 index 000000000000000..3511fb44cdfb241 --- /dev/null +++ b/src/core/public/http/_import_objects.ndjson @@ -0,0 +1 @@ +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"Log Agents","uiStateJSON":"{}","visState":"{\"title\":\"Log Agents\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{\"text\":\"agent.raw: Descending\"}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"agent.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}"},"id":"082f1d60-a2e7-11e7-bb30-233be9be6a15","migrationVersion":{"visualization":"7.0.0"},"references":[{"id":"f1e4c910-a2e6-11e7-bb30-233be9be6a15","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"visualization","version":1} diff --git a/src/core/public/http/fetch.ts b/src/core/public/http/fetch.ts new file mode 100644 index 000000000000000..9bfc13820cb5522 --- /dev/null +++ b/src/core/public/http/fetch.ts @@ -0,0 +1,91 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { merge } from 'lodash'; +import { format } from 'url'; + +import { HttpFetchOptions, HttpBody, Deps } from './types'; +import { HttpFetchError } from './http_fetch_error'; + +const JSON_CONTENT = /^(application\/(json|x-javascript)|text\/(x-)?javascript|x-json)(;.*)?$/; +const NDJSON_CONTENT = /^(application\/ndjson)(;.*)?$/; + +export const setup = ({ basePath, injectedMetadata }: Deps) => { + async function fetch(path: string, options: HttpFetchOptions = {}): Promise { + const { query, prependBasePath, ...fetchOptions } = merge( + { + method: 'GET', + credentials: 'same-origin', + prependBasePath: true, + headers: { + 'kbn-version': injectedMetadata.getKibanaVersion(), + 'Content-Type': 'application/json', + }, + }, + options + ); + const url = format({ + pathname: prependBasePath ? basePath.addToPath(path) : path, + query, + }); + + if ( + options.headers && + 'Content-Type' in options.headers && + options.headers['Content-Type'] === undefined + ) { + delete fetchOptions.headers['Content-Type']; + } + + let response; + let body = null; + + try { + response = await window.fetch(url, fetchOptions as RequestInit); + } catch (err) { + throw new HttpFetchError(err.message); + } + + const contentType = response.headers.get('Content-Type') || ''; + + try { + if (NDJSON_CONTENT.test(contentType)) { + body = await response.blob(); + } else if (JSON_CONTENT.test(contentType)) { + body = await response.json(); + } else { + body = await response.text(); + } + } catch (err) { + throw new HttpFetchError(err.message, response, body); + } + + if (!response.ok) { + throw new HttpFetchError(response.statusText, response, body); + } + + return body; + } + + function shorthand(method: string) { + return (path: string, options: HttpFetchOptions = {}) => fetch(path, { ...options, method }); + } + + return { fetch, shorthand }; +}; diff --git a/src/legacy/ui/public/kfetch/kfetch_abortable.ts b/src/core/public/http/http_fetch_error.ts similarity index 57% rename from src/legacy/ui/public/kfetch/kfetch_abortable.ts rename to src/core/public/http/http_fetch_error.ts index 11057054f330f31..a73fb7e3ffbd409 100644 --- a/src/legacy/ui/public/kfetch/kfetch_abortable.ts +++ b/src/core/public/http/http_fetch_error.ts @@ -17,29 +17,14 @@ * under the License. */ -import { kfetch, KFetchKibanaOptions, KFetchOptions } from './kfetch'; +export class HttpFetchError extends Error { + constructor(message: string, public readonly response?: Response, public readonly body?: any) { + super(message); -type Omit = Pick>; - -function createAbortable() { - const abortController = new AbortController(); - const { signal, abort } = abortController; - - return { - signal, - abort: abort.bind(abortController), - }; -} - -export function kfetchAbortable( - fetchOptions?: Omit, - kibanaOptions?: KFetchKibanaOptions -) { - const { signal, abort } = createAbortable(); - const fetching = kfetch({ ...fetchOptions, signal }, kibanaOptions); - - return { - fetching, - abort, - }; + // captureStackTrace is only available in the V8 engine, so any browser using + // a different JS engine won't have access to this method. + if (Error.captureStackTrace) { + Error.captureStackTrace(this, HttpFetchError); + } + } } diff --git a/src/core/public/http/http_service.mock.ts b/src/core/public/http/http_service.mock.ts index 3146ad4d2217f38..1683af857d7ae60 100644 --- a/src/core/public/http/http_service.mock.ts +++ b/src/core/public/http/http_service.mock.ts @@ -16,25 +16,27 @@ * specific language governing permissions and limitations * under the License. */ -import { HttpService, HttpSetup } from './http_service'; -const createSetupContractMock = () => { - const setupContract: jest.Mocked = { - addLoadingCount: jest.fn(), - getLoadingCount$: jest.fn(), - }; - return setupContract; -}; +import { HttpService, HttpSetup, HttpStart } from './http_service'; -type HttpServiceContract = PublicMethodsOf; -const createMock = () => { - const mocked: jest.Mocked = { - setup: jest.fn(), - stop: jest.fn(), - }; - mocked.setup.mockReturnValue(createSetupContractMock()); - return mocked; -}; +const createSetupContractMock = (): jest.Mocked => ({ + fetch: jest.fn(), + get: jest.fn(), + head: jest.fn(), + post: jest.fn(), + put: jest.fn(), + patch: jest.fn(), + delete: jest.fn(), + options: jest.fn(), + addLoadingCount: jest.fn(), + getLoadingCount$: jest.fn(), +}); +const createStartContractMock = (): jest.Mocked => undefined; +const createMock = (): jest.Mocked> => ({ + setup: jest.fn().mockReturnValue(createSetupContractMock()), + start: jest.fn().mockReturnValue(createStartContractMock()), + stop: jest.fn(), +}); export const httpServiceMock = { create: createMock, diff --git a/src/core/public/http/http_service.test.ts b/src/core/public/http/http_service.test.ts index 35b674aca9b9f36..51f4af44d313d3c 100644 --- a/src/core/public/http/http_service.test.ts +++ b/src/core/public/http/http_service.test.ts @@ -19,35 +19,254 @@ import * as Rx from 'rxjs'; import { toArray } from 'rxjs/operators'; +// @ts-ignore +import fetchMock from 'fetch-mock/es5/client'; +import { BasePathService } from '../base_path/base_path_service'; import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock'; +import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; import { HttpService } from './http_service'; +import { readFileSync } from 'fs'; +import { join } from 'path'; function setupService() { - const service = new HttpService(); + const httpService = new HttpService(); const fatalErrors = fatalErrorsServiceMock.createSetupContract(); - const setup = service.setup({ fatalErrors }); + const injectedMetadata = injectedMetadataServiceMock.createSetupContract(); - return { service, fatalErrors, setup }; + injectedMetadata.getBasePath.mockReturnValueOnce('http://localhost/myBase'); + + const basePath = new BasePathService().setup({ injectedMetadata }); + const http = httpService.setup({ basePath, fatalErrors, injectedMetadata }); + + return { httpService, fatalErrors, http }; } +describe('http requests', async () => { + afterEach(() => { + fetchMock.restore(); + }); + + it('should use supplied request method', async () => { + const { http } = setupService(); + + fetchMock.post('*', {}); + await http.fetch('/my/path', { method: 'POST' }); + + expect(fetchMock.lastOptions()!.method).toBe('POST'); + }); + + it('should use supplied Content-Type', async () => { + const { http } = setupService(); + + fetchMock.get('*', {}); + await http.fetch('/my/path', { headers: { 'Content-Type': 'CustomContentType' } }); + + expect(fetchMock.lastOptions()!.headers).toMatchObject({ + 'Content-Type': 'CustomContentType', + }); + }); + + it('should use supplied pathname and querystring', async () => { + const { http } = setupService(); + + fetchMock.get('*', {}); + await http.fetch('/my/path', { query: { a: 'b' } }); + + expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path?a=b'); + }); + + it('should use supplied headers', async () => { + const { http } = setupService(); + + fetchMock.get('*', {}); + await http.fetch('/my/path', { + headers: { myHeader: 'foo' }, + }); + + expect(fetchMock.lastOptions()!.headers).toEqual({ + 'Content-Type': 'application/json', + 'kbn-version': 'kibanaVersion', + myHeader: 'foo', + }); + }); + + it('should return response', async () => { + const { http } = setupService(); + + fetchMock.get('*', { foo: 'bar' }); + + const json = await http.fetch('/my/path'); + + expect(json).toEqual({ foo: 'bar' }); + }); + + it('should prepend url with basepath by default', async () => { + const { http } = setupService(); + + fetchMock.get('*', {}); + await http.fetch('/my/path'); + + expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path'); + }); + + it('should not prepend url with basepath when disabled', async () => { + const { http } = setupService(); + + fetchMock.get('*', {}); + await http.fetch('my/path', { prependBasePath: false }); + + expect(fetchMock.lastUrl()).toBe('/my/path'); + }); + + it('should make request with defaults', async () => { + const { http } = setupService(); + + fetchMock.get('*', {}); + await http.fetch('/my/path'); + + expect(fetchMock.lastOptions()!).toMatchObject({ + method: 'GET', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + 'kbn-version': 'kibanaVersion', + }, + }); + }); + + it('should reject on network error', async () => { + const { http } = setupService(); + + expect.assertions(1); + fetchMock.get('*', { status: 500 }); + + await expect(http.fetch('/my/path')).rejects.toThrow(/Internal Server Error/); + }); + + it('should contain error message when throwing response', async () => { + const { http } = setupService(); + + fetchMock.get('*', { status: 404, body: { foo: 'bar' } }); + + await expect(http.fetch('/my/path')).rejects.toMatchObject({ + message: 'Not Found', + body: { + foo: 'bar', + }, + response: { + status: 404, + url: 'http://localhost/myBase/my/path', + }, + }); + }); + + it('should support get() helper', async () => { + const { http } = setupService(); + + fetchMock.get('*', {}); + await http.get('/my/path', { method: 'POST' }); + + expect(fetchMock.lastOptions()!.method).toBe('GET'); + }); + + it('should support head() helper', async () => { + const { http } = setupService(); + + fetchMock.head('*', {}); + await http.head('/my/path', { method: 'GET' }); + + expect(fetchMock.lastOptions()!.method).toBe('HEAD'); + }); + + it('should support post() helper', async () => { + const { http } = setupService(); + + fetchMock.post('*', {}); + await http.post('/my/path', { method: 'GET', body: '{}' }); + + expect(fetchMock.lastOptions()!.method).toBe('POST'); + }); + + it('should support put() helper', async () => { + const { http } = setupService(); + + fetchMock.put('*', {}); + await http.put('/my/path', { method: 'GET', body: '{}' }); + + expect(fetchMock.lastOptions()!.method).toBe('PUT'); + }); + + it('should support patch() helper', async () => { + const { http } = setupService(); + + fetchMock.patch('*', {}); + await http.patch('/my/path', { method: 'GET', body: '{}' }); + + expect(fetchMock.lastOptions()!.method).toBe('PATCH'); + }); + + it('should support delete() helper', async () => { + const { http } = setupService(); + + fetchMock.delete('*', {}); + await http.delete('/my/path', { method: 'GET' }); + + expect(fetchMock.lastOptions()!.method).toBe('DELETE'); + }); + + it('should support options() helper', async () => { + const { http } = setupService(); + + fetchMock.mock('*', { method: 'OPTIONS' }); + await http.options('/my/path', { method: 'GET' }); + + expect(fetchMock.lastOptions()!.method).toBe('OPTIONS'); + }); + + it('should make requests for NDJSON content', async () => { + const { http } = setupService(); + const content = readFileSync(join(__dirname, '_import_objects.ndjson'), { encoding: 'utf-8' }); + const body = new FormData(); + + body.append('file', content); + fetchMock.post('*', { + body: content, + headers: { 'Content-Type': 'application/ndjson' }, + }); + + const data = await http.post('/my/path', { + body, + headers: { + 'Content-Type': undefined, + }, + }); + + expect(data).toBeInstanceOf(Blob); + + const ndjson = await new Response(data).text(); + + expect(ndjson).toEqual(content); + }); +}); + describe('addLoadingCount()', async () => { it('subscribes to passed in sources, unsubscribes on stop', () => { - const { service, setup } = setupService(); + const { httpService, http } = setupService(); const unsubA = jest.fn(); const subA = jest.fn().mockReturnValue(unsubA); - setup.addLoadingCount(new Rx.Observable(subA)); + http.addLoadingCount(new Rx.Observable(subA)); expect(subA).toHaveBeenCalledTimes(1); expect(unsubA).not.toHaveBeenCalled(); const unsubB = jest.fn(); const subB = jest.fn().mockReturnValue(unsubB); - setup.addLoadingCount(new Rx.Observable(subB)); + http.addLoadingCount(new Rx.Observable(subB)); expect(subB).toHaveBeenCalledTimes(1); expect(unsubB).not.toHaveBeenCalled(); - service.stop(); + httpService.stop(); expect(subA).toHaveBeenCalledTimes(1); expect(unsubA).toHaveBeenCalledTimes(1); @@ -56,35 +275,35 @@ describe('addLoadingCount()', async () => { }); it('adds a fatal error if source observables emit an error', async () => { - const { setup, fatalErrors } = setupService(); + const { http, fatalErrors } = setupService(); - setup.addLoadingCount(Rx.throwError(new Error('foo bar'))); + http.addLoadingCount(Rx.throwError(new Error('foo bar'))); expect(fatalErrors.add.mock.calls).toMatchSnapshot(); }); it('adds a fatal error if source observable emits a negative number', async () => { - const { setup, fatalErrors } = setupService(); + const { http, fatalErrors } = setupService(); - setup.addLoadingCount(Rx.of(1, 2, 3, 4, -9)); + http.addLoadingCount(Rx.of(1, 2, 3, 4, -9)); expect(fatalErrors.add.mock.calls).toMatchSnapshot(); }); }); describe('getLoadingCount$()', async () => { it('emits 0 initially, the right count when sources emit their own count, and ends with zero', async () => { - const { service, setup } = setupService(); + const { httpService, http } = setupService(); const countA$ = new Rx.Subject(); const countB$ = new Rx.Subject(); const countC$ = new Rx.Subject(); - const promise = setup + const promise = http .getLoadingCount$() .pipe(toArray()) .toPromise(); - setup.addLoadingCount(countA$); - setup.addLoadingCount(countB$); - setup.addLoadingCount(countC$); + http.addLoadingCount(countA$); + http.addLoadingCount(countB$); + http.addLoadingCount(countC$); countA$.next(100); countB$.next(10); @@ -94,20 +313,20 @@ describe('getLoadingCount$()', async () => { countC$.complete(); countB$.next(0); - service.stop(); + httpService.stop(); expect(await promise).toMatchSnapshot(); }); it('only emits when loading count changes', async () => { - const { service, setup } = setupService(); + const { httpService, http } = setupService(); const count$ = new Rx.Subject(); - const promise = setup + const promise = http .getLoadingCount$() .pipe(toArray()) .toPromise(); - setup.addLoadingCount(count$); + http.addLoadingCount(count$); count$.next(0); count$.next(0); count$.next(0); @@ -115,7 +334,7 @@ describe('getLoadingCount$()', async () => { count$.next(0); count$.next(1); count$.next(1); - service.stop(); + httpService.stop(); expect(await promise).toMatchSnapshot(); }); diff --git a/src/core/public/http/http_service.ts b/src/core/public/http/http_service.ts index 4121181cf80ec51..5a73ade939365df 100644 --- a/src/core/public/http/http_service.ts +++ b/src/core/public/http/http_service.ts @@ -28,19 +28,26 @@ import { tap, } from 'rxjs/operators'; -import { FatalErrorsSetup } from '../fatal_errors'; - -interface Deps { - fatalErrors: FatalErrorsSetup; -} +import { Deps } from './types'; +import { setup } from './fetch'; /** @internal */ export class HttpService { private readonly loadingCount$ = new Rx.BehaviorSubject(0); private readonly stop$ = new Rx.Subject(); - public setup({ fatalErrors }: Deps) { + public setup(deps: Deps) { + const { fetch, shorthand } = setup(deps); + return { + fetch, + delete: shorthand('DELETE'), + get: shorthand('GET'), + head: shorthand('HEAD'), + options: shorthand('OPTIONS'), + patch: shorthand('PATCH'), + post: shorthand('POST'), + put: shorthand('PUT'), addLoadingCount: (count$: Rx.Observable) => { count$ .pipe( @@ -67,7 +74,7 @@ export class HttpService { this.loadingCount$.next(this.loadingCount$.getValue() + delta); }, error: error => { - fatalErrors.add(error); + deps.fatalErrors.add(error); }, }); }, @@ -78,6 +85,9 @@ export class HttpService { }; } + // eslint-disable-next-line no-unused-params + public start(deps: Deps) {} + public stop() { this.stop$.next(); this.loadingCount$.complete(); @@ -86,3 +96,4 @@ export class HttpService { /** @public */ export type HttpSetup = ReturnType; +export type HttpStart = ReturnType; diff --git a/src/core/public/http/index.ts b/src/core/public/http/index.ts index 24ba49a4dfcac11..ee17c225c711b39 100644 --- a/src/core/public/http/index.ts +++ b/src/core/public/http/index.ts @@ -18,3 +18,4 @@ */ export { HttpService, HttpSetup } from './http_service'; +export { HttpFetchError } from './http_fetch_error'; diff --git a/src/core/public/http/types.ts b/src/core/public/http/types.ts new file mode 100644 index 000000000000000..05f6ab502246ddb --- /dev/null +++ b/src/core/public/http/types.ts @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { BasePathSetup } from '../base_path'; +import { InjectedMetadataSetup } from '../injected_metadata'; +import { FatalErrorsSetup } from '../fatal_errors'; + +export interface HttpHeadersInit { + [name: string]: any; +} +export interface HttpRequestInit { + body?: BodyInit | null; + cache?: RequestCache; + credentials?: RequestCredentials; + headers?: HttpHeadersInit; + integrity?: string; + keepalive?: boolean; + method?: string; + mode?: RequestMode; + redirect?: RequestRedirect; + referrer?: string; + referrerPolicy?: ReferrerPolicy; + signal?: AbortSignal | null; + window?: any; +} +export interface Deps { + basePath: BasePathSetup; + injectedMetadata: InjectedMetadataSetup; + fatalErrors: FatalErrorsSetup; +} +export interface HttpFetchQuery { + [key: string]: string | number | boolean | undefined; +} +export interface HttpFetchOptions extends HttpRequestInit { + query?: HttpFetchQuery; + prependBasePath?: boolean; + headers?: HttpHeadersInit; +} +export type HttpBody = BodyInit | null; diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts index bc1aff696ed6366..6e6f25649742645 100644 --- a/src/core/public/legacy/legacy_service.ts +++ b/src/core/public/legacy/legacy_service.ts @@ -70,6 +70,7 @@ export class LegacyPlatformService { require('ui/metadata').__newPlatformSetup__(injectedMetadata.getLegacyMetadata()); require('ui/i18n').__newPlatformSetup__(i18n.Context); require('ui/notify/fatal_error').__newPlatformSetup__(fatalErrors); + require('ui/kfetch').__newPlatformSetup__(http); require('ui/notify/toasts').__newPlatformSetup__(notifications.toasts); require('ui/chrome/api/loading_count').__newPlatformSetup__(http); require('ui/chrome/api/base_path').__newPlatformSetup__(basePath); diff --git a/src/legacy/core_plugins/data/public/query_bar/components/query_bar.test.mocks.ts b/src/legacy/core_plugins/data/public/query_bar/components/query_bar.test.mocks.ts index ac1399addb70c10..0f0d5e1591b1751 100644 --- a/src/legacy/core_plugins/data/public/query_bar/components/query_bar.test.mocks.ts +++ b/src/legacy/core_plugins/data/public/query_bar/components/query_bar.test.mocks.ts @@ -17,6 +17,9 @@ * under the License. */ +import { createKfetch } from 'ui/kfetch/kfetch'; +import { setup } from '../../../../../../test_utils/public/kfetch_test_setup'; + const mockChromeFactory = jest.fn(() => { return { getBasePath: () => `foo`, @@ -47,6 +50,7 @@ export const mockPersistedLogFactory = jest.fn Promise.resolve([])); const mockAutocompleteProvider = jest.fn(() => mockGetAutocompleteSuggestions); export const mockGetAutocompleteProvider = jest.fn(() => mockAutocompleteProvider); +const mockKfetch = jest.fn(() => createKfetch(setup().http)); jest.mock('ui/chrome', () => mockChromeFactory()); jest.mock('ui/kfetch', () => ({ @@ -63,6 +67,10 @@ jest.mock('ui/metadata', () => ({ jest.mock('ui/autocomplete_providers', () => ({ getAutocompleteProvider: mockGetAutocompleteProvider, })); +jest.mock('ui/kfetch', () => ({ + __newPlatformSetup__: jest.fn(), + kfetch: mockKfetch, +})); import _ from 'lodash'; // Using doMock to avoid hoisting so that I can override only the debounce method in lodash diff --git a/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.test.js b/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.test.js index 291eff2454c2923..6fcbacede2a65b9 100644 --- a/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.test.js +++ b/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.test.js @@ -17,20 +17,18 @@ * under the License. */ -jest.mock('../chrome', () => ({ - addBasePath: path => `myBase/${path}`, -})); -jest.mock('../metadata', () => ({ - metadata: { - version: 'my-version', - }, -})); - +// @ts-ignore import fetchMock from 'fetch-mock/es5/client'; -import { kfetch } from 'ui/kfetch'; +import { __newPlatformSetup__, kfetch } from '../kfetch'; +import { setup } from '../../../../test_utils/public/kfetch_test_setup'; + import { isAutoCreateIndexError } from './error_auto_create_index'; describe('isAutoCreateIndexError correctly handles KFetchError thrown by kfetch', () => { + beforeAll(() => { + __newPlatformSetup__(setup().http); + }); + describe('404', () => { beforeEach(() => { fetchMock.post({ @@ -45,7 +43,7 @@ describe('isAutoCreateIndexError correctly handles KFetchError thrown by kfetch' test('should return false', async () => { expect.assertions(1); try { - await kfetch({ method: 'POST', pathname: 'my/path' }); + await kfetch({ method: 'POST', pathname: '/my/path' }); } catch (kfetchError) { expect(isAutoCreateIndexError(kfetchError)).toBe(false); } @@ -66,7 +64,7 @@ describe('isAutoCreateIndexError correctly handles KFetchError thrown by kfetch' test('should return false', async () => { expect.assertions(1); try { - await kfetch({ method: 'POST', pathname: 'my/path' }); + await kfetch({ method: 'POST', pathname: '/my/path' }); } catch (kfetchError) { expect(isAutoCreateIndexError(kfetchError)).toBe(false); } @@ -90,7 +88,7 @@ describe('isAutoCreateIndexError correctly handles KFetchError thrown by kfetch' test('should return true', async () => { expect.assertions(1); try { - await kfetch({ method: 'POST', pathname: 'my/path' }); + await kfetch({ method: 'POST', pathname: '/my/path' }); } catch (kfetchError) { expect(isAutoCreateIndexError(kfetchError)).toBe(true); } diff --git a/src/legacy/ui/public/kfetch/_import_objects.ndjson b/src/legacy/ui/public/kfetch/_import_objects.ndjson new file mode 100644 index 000000000000000..3511fb44cdfb241 --- /dev/null +++ b/src/legacy/ui/public/kfetch/_import_objects.ndjson @@ -0,0 +1 @@ +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"Log Agents","uiStateJSON":"{}","visState":"{\"title\":\"Log Agents\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{\"text\":\"agent.raw: Descending\"}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"agent.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}"},"id":"082f1d60-a2e7-11e7-bb30-233be9be6a15","migrationVersion":{"visualization":"7.0.0"},"references":[{"id":"f1e4c910-a2e6-11e7-bb30-233be9be6a15","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"visualization","version":1} diff --git a/src/legacy/ui/public/kfetch/index.ts b/src/legacy/ui/public/kfetch/index.ts index 234304b5950aa45..011321c83c4cd02 100644 --- a/src/legacy/ui/public/kfetch/index.ts +++ b/src/legacy/ui/public/kfetch/index.ts @@ -17,5 +17,23 @@ * under the License. */ -export { kfetch, addInterceptor, KFetchOptions, KFetchQuery } from './kfetch'; -export { kfetchAbortable } from './kfetch_abortable'; +import { createKfetch, KFetchKibanaOptions, KFetchOptions } from './kfetch'; +export { addInterceptor, KFetchOptions, KFetchQuery } from './kfetch'; + +import { HttpSetup } from '../../../../core/public'; + +let http: HttpSetup; +let kfetchInstance: (options: KFetchOptions, kfetchOptions?: KFetchKibanaOptions) => any; + +export function __newPlatformSetup__(httpSetup: HttpSetup) { + if (http) { + throw new Error('ui/kfetch already initialized with New Platform APIs'); + } + + http = httpSetup; + kfetchInstance = createKfetch(http); +} + +export const kfetch = (options: KFetchOptions, kfetchOptions?: KFetchKibanaOptions) => { + return kfetchInstance(options, kfetchOptions); +}; diff --git a/src/legacy/ui/public/kfetch/kfetch.test.ts b/src/legacy/ui/public/kfetch/kfetch.test.ts index 8f8cc807911a3af..79bca9da1d273de 100644 --- a/src/legacy/ui/public/kfetch/kfetch.test.ts +++ b/src/legacy/ui/public/kfetch/kfetch.test.ts @@ -17,28 +17,20 @@ * under the License. */ -jest.mock('../chrome', () => ({ - addBasePath: (path: string) => `http://localhost/myBase/${path}`, -})); - -jest.mock('../metadata', () => ({ - metadata: { - version: 'my-version', - }, -})); - // @ts-ignore import fetchMock from 'fetch-mock/es5/client'; -import { - addInterceptor, - Interceptor, - kfetch, - resetInterceptors, - withDefaultOptions, -} from './kfetch'; +import { readFileSync } from 'fs'; +import { join } from 'path'; +import { __newPlatformSetup__, addInterceptor, kfetch, KFetchOptions } from '.'; +import { Interceptor, resetInterceptors, withDefaultOptions } from './kfetch'; import { KFetchError } from './kfetch_error'; +import { setup } from '../../../../test_utils/public/kfetch_test_setup'; describe('kfetch', () => { + beforeAll(() => { + __newPlatformSetup__(setup().http); + }); + afterEach(() => { fetchMock.restore(); resetInterceptors(); @@ -46,13 +38,13 @@ describe('kfetch', () => { it('should use supplied request method', async () => { fetchMock.post('*', {}); - await kfetch({ pathname: 'my/path', method: 'POST' }); + await kfetch({ pathname: '/my/path', method: 'POST' }); expect(fetchMock.lastOptions()!.method).toBe('POST'); }); it('should use supplied Content-Type', async () => { fetchMock.get('*', {}); - await kfetch({ pathname: 'my/path', headers: { 'Content-Type': 'CustomContentType' } }); + await kfetch({ pathname: '/my/path', headers: { 'Content-Type': 'CustomContentType' } }); expect(fetchMock.lastOptions()!.headers).toMatchObject({ 'Content-Type': 'CustomContentType', }); @@ -60,64 +52,88 @@ describe('kfetch', () => { it('should use supplied pathname and querystring', async () => { fetchMock.get('*', {}); - await kfetch({ pathname: 'my/path', query: { a: 'b' } }); + await kfetch({ pathname: '/my/path', query: { a: 'b' } }); expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path?a=b'); }); it('should use supplied headers', async () => { fetchMock.get('*', {}); await kfetch({ - pathname: 'my/path', + pathname: '/my/path', headers: { myHeader: 'foo' }, }); expect(fetchMock.lastOptions()!.headers).toEqual({ 'Content-Type': 'application/json', - 'kbn-version': 'my-version', + 'kbn-version': 'kibanaVersion', myHeader: 'foo', }); }); it('should return response', async () => { fetchMock.get('*', { foo: 'bar' }); - const res = await kfetch({ pathname: 'my/path' }); + const res = await kfetch({ pathname: '/my/path' }); expect(res).toEqual({ foo: 'bar' }); }); it('should prepend url with basepath by default', async () => { fetchMock.get('*', {}); - await kfetch({ pathname: 'my/path' }); + await kfetch({ pathname: '/my/path' }); expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path'); }); it('should not prepend url with basepath when disabled', async () => { fetchMock.get('*', {}); - await kfetch({ pathname: 'my/path' }, { prependBasePath: false }); + await kfetch({ pathname: '/my/path' }, { prependBasePath: false }); expect(fetchMock.lastUrl()).toBe('/my/path'); }); it('should make request with defaults', async () => { fetchMock.get('*', {}); - await kfetch({ pathname: 'my/path' }); + await kfetch({ pathname: '/my/path' }); - expect(fetchMock.lastOptions()!).toEqual({ + expect(fetchMock.lastOptions()!).toMatchObject({ method: 'GET', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', - 'kbn-version': 'my-version', + 'kbn-version': 'kibanaVersion', }, }); }); + it('should make requests for NDJSON content', async () => { + const content = readFileSync(join(__dirname, '_import_objects.ndjson'), { encoding: 'utf-8' }); + + fetchMock.post('*', { + body: content, + headers: { 'Content-Type': 'application/ndjson' }, + }); + + const data = await kfetch({ + method: 'POST', + pathname: '/my/path', + body: content, + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + expect(data).toBeInstanceOf(Blob); + + const ndjson = await new Response(data).text(); + + expect(ndjson).toEqual(content); + }); + it('should reject on network error', async () => { expect.assertions(1); - fetchMock.get('*', { throws: new Error('Network issue') }); + fetchMock.get('*', { status: 500 }); try { - await kfetch({ pathname: 'my/path' }); + await kfetch({ pathname: '/my/path' }); } catch (e) { - expect(e.message).toBe('Network issue'); + expect(e.message).toBe('Internal Server Error'); } }); @@ -126,7 +142,7 @@ describe('kfetch', () => { beforeEach(async () => { fetchMock.get('*', { status: 404, body: { foo: 'bar' } }); try { - await kfetch({ pathname: 'my/path' }); + await kfetch({ pathname: '/my/path' }); } catch (e) { error = e; } @@ -154,7 +170,7 @@ describe('kfetch', () => { fetchMock.get('*', { foo: 'bar' }); interceptorCalls = mockInterceptorCalls([{}, {}, {}]); - resp = await kfetch({ pathname: 'my/path' }); + resp = await kfetch({ pathname: '/my/path' }); }); it('should call interceptors in correct order', () => { @@ -185,12 +201,12 @@ describe('kfetch', () => { fetchMock.get('*', { foo: 'bar' }); interceptorCalls = mockInterceptorCalls([ - { requestError: () => ({}) }, + { requestError: () => ({ pathname: '/my/path' } as KFetchOptions) }, { request: () => Promise.reject(new Error('Error in request')) }, {}, ]); - resp = await kfetch({ pathname: 'my/path' }); + resp = await kfetch({ pathname: '/my/path' }); }); it('should call interceptors in correct order', () => { @@ -227,7 +243,7 @@ describe('kfetch', () => { ]); try { - await kfetch({ pathname: 'my/path' }); + await kfetch({ pathname: '/my/path' }); } catch (e) { error = e; } @@ -267,7 +283,7 @@ describe('kfetch', () => { ]); try { - await kfetch({ pathname: 'my/path' }); + await kfetch({ pathname: '/my/path' }); } catch (e) { error = e; } @@ -313,7 +329,7 @@ describe('kfetch', () => { {}, ]); - resp = await kfetch({ pathname: 'my/path' }); + resp = await kfetch({ pathname: '/my/path' }); }); it('should call in correct order', () => { @@ -351,7 +367,7 @@ describe('kfetch', () => { }), }); - resp = await kfetch({ pathname: 'my/path' }); + resp = await kfetch({ pathname: '/my/path' }); }); it('should modify request', () => { @@ -386,7 +402,7 @@ describe('kfetch', () => { }), }); - resp = await kfetch({ pathname: 'my/path' }); + resp = await kfetch({ pathname: '/my/path' }); }); it('should modify request', () => { @@ -453,6 +469,7 @@ function mockInterceptorCalls(interceptors: Interceptor[]) { describe('withDefaultOptions', () => { it('should remove undefined query params', () => { const { query } = withDefaultOptions({ + pathname: '/withDefaultOptions', query: { foo: 'bar', param1: (undefined as any) as string, @@ -464,9 +481,10 @@ describe('withDefaultOptions', () => { }); it('should add default options', () => { - expect(withDefaultOptions({})).toEqual({ + expect(withDefaultOptions({ pathname: '/addDefaultOptions' })).toEqual({ + pathname: '/addDefaultOptions', credentials: 'same-origin', - headers: { 'Content-Type': 'application/json', 'kbn-version': 'my-version' }, + headers: { 'Content-Type': 'application/json' }, method: 'GET', }); }); diff --git a/src/legacy/ui/public/kfetch/kfetch.ts b/src/legacy/ui/public/kfetch/kfetch.ts index 93d736bd8666eda..cb96e03eb132834 100644 --- a/src/legacy/ui/public/kfetch/kfetch.ts +++ b/src/legacy/ui/public/kfetch/kfetch.ts @@ -19,17 +19,18 @@ import { merge } from 'lodash'; // @ts-ignore not really worth typing -import { metadata } from 'ui/metadata'; -import url from 'url'; -import chrome from '../chrome'; import { KFetchError } from './kfetch_error'; +import { HttpSetup } from '../../../../core/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { HttpRequestInit } from '../../../../core/public/http/types'; + export interface KFetchQuery { [key: string]: string | number | boolean | undefined; } -export interface KFetchOptions extends RequestInit { - pathname?: string; +export interface KFetchOptions extends HttpRequestInit { + pathname: string; query?: KFetchQuery; } @@ -48,32 +49,21 @@ const interceptors: Interceptor[] = []; export const resetInterceptors = () => (interceptors.length = 0); export const addInterceptor = (interceptor: Interceptor) => interceptors.push(interceptor); -export async function kfetch( - options: KFetchOptions, - { prependBasePath = true }: KFetchKibanaOptions = {} -) { - const combinedOptions = withDefaultOptions(options); - const promise = requestInterceptors(combinedOptions).then( - ({ pathname, query, ...restOptions }) => { - const fullUrl = url.format({ - pathname: prependBasePath ? chrome.addBasePath(pathname) : pathname, - query, - }); - - return window.fetch(fullUrl, restOptions).then(async res => { - if (!res.ok) { - throw new KFetchError(res, await getBodyAsJson(res)); - } - const contentType = res.headers.get('content-type'); - if (contentType && contentType.split(';')[0] === 'application/ndjson') { - return await getBodyAsBlob(res); - } - return await getBodyAsJson(res); - }); - } - ); - - return responseInterceptors(promise); +export function createKfetch(http: HttpSetup) { + return function kfetch( + options: KFetchOptions, + { prependBasePath = true }: KFetchKibanaOptions = {} + ) { + return responseInterceptors( + requestInterceptors(withDefaultOptions(options)) + .then(({ pathname, ...restOptions }) => + http.fetch(pathname, { ...restOptions, prependBasePath }) + ) + .catch(err => { + throw new KFetchError(err.response || { statusText: err.message }, err.body); + }) + ); + }; } // Request/response interceptors are called in opposite orders. @@ -91,36 +81,29 @@ function responseInterceptors(responsePromise: Promise) { }, responsePromise); } -async function getBodyAsJson(res: Response) { - try { - return await res.json(); - } catch (e) { - return null; - } -} - -async function getBodyAsBlob(res: Response) { - try { - return await res.blob(); - } catch (e) { - return null; - } -} - export function withDefaultOptions(options?: KFetchOptions): KFetchOptions { - return merge( + const withDefaults = merge( { method: 'GET', credentials: 'same-origin', headers: { - ...(options && options.headers && options.headers.hasOwnProperty('Content-Type') - ? {} - : { - 'Content-Type': 'application/json', - }), - 'kbn-version': metadata.version, + 'Content-Type': 'application/json', }, }, options - ); + ) as KFetchOptions; + + if ( + options && + options.headers && + 'Content-Type' in options.headers && + options.headers['Content-Type'] === undefined + ) { + // TS thinks headers could be undefined here, but that isn't possible because + // of the merge above. + // @ts-ignore + withDefaults.headers['Content-Type'] = undefined; + } + + return withDefaults; } diff --git a/src/legacy/ui/public/kfetch/kfetch_abortable.test.ts b/src/legacy/ui/public/kfetch/kfetch_abortable.test.ts deleted file mode 100644 index bb1c5ac07252433..000000000000000 --- a/src/legacy/ui/public/kfetch/kfetch_abortable.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -jest.mock('../chrome', () => ({ - addBasePath: (path: string) => `http://localhost/myBase/${path}`, -})); - -jest.mock('../metadata', () => ({ - metadata: { - version: 'my-version', - }, -})); - -import { kfetchAbortable } from './kfetch_abortable'; - -describe('kfetchAbortable', () => { - it('should return an object with a fetching promise and an abort callback', () => { - const { fetching, abort } = kfetchAbortable({ pathname: 'my/path' }); - expect(typeof fetching.then).toBe('function'); - expect(typeof fetching.catch).toBe('function'); - expect(typeof abort).toBe('function'); - }); -}); diff --git a/src/test_utils/public/kfetch_test_setup.ts b/src/test_utils/public/kfetch_test_setup.ts new file mode 100644 index 000000000000000..a102ceb89faf233 --- /dev/null +++ b/src/test_utils/public/kfetch_test_setup.ts @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* eslint-disable @kbn/eslint/no-restricted-paths */ +import { HttpService } from '../../core/public/http'; +import { BasePathService } from '../../core/public/base_path'; +import { fatalErrorsServiceMock } from '../../core/public/fatal_errors/fatal_errors_service.mock'; +import { injectedMetadataServiceMock } from '../../core/public/injected_metadata/injected_metadata_service.mock'; +/* eslint-enable @kbn/eslint/no-restricted-paths */ + +export function setup() { + const httpService = new HttpService(); + const fatalErrors = fatalErrorsServiceMock.createSetupContract(); + const injectedMetadata = injectedMetadataServiceMock.createSetupContract(); + + injectedMetadata.getBasePath.mockReturnValue('http://localhost/myBase'); + + const basePath = new BasePathService().setup({ injectedMetadata }); + const http = httpService.setup({ basePath, fatalErrors, injectedMetadata }); + + return { httpService, fatalErrors, http }; +} diff --git a/x-pack/plugins/rollup/public/search/rollup_search_strategy.js b/x-pack/plugins/rollup/public/search/rollup_search_strategy.js index 826292c67b0a401..abc0bc620b81a1d 100644 --- a/x-pack/plugins/rollup/public/search/rollup_search_strategy.js +++ b/x-pack/plugins/rollup/public/search/rollup_search_strategy.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { kfetchAbortable } from 'ui/kfetch'; +import { kfetch } from 'ui/kfetch'; import { SearchError, getSearchErrorType } from 'ui/courier'; function getAllFetchParams(searchRequests, Promise) { @@ -95,20 +95,18 @@ export const rollupSearchStrategy = { failedSearchRequests, } = await serializeAllFetchParams(allFetchParams, searchRequests); - const { - fetching, - abort, - } = kfetchAbortable({ + const controller = new AbortController(); + const promise = kfetch({ + signal: controller.signal, pathname: '../api/rollup/search', method: 'POST', body: serializedFetchParams, }); return { - searching: new Promise((resolve, reject) => { - fetching.then(result => { - resolve(shimHitsInFetchResponse(result)); - }).catch(error => { + searching: promise + .then(shimHitsInFetchResponse) + .catch(error => { const { body: { statusText, error: title, message }, res: { url }, @@ -123,10 +121,9 @@ export const rollupSearchStrategy = { type: getSearchErrorType({ message }), }); - reject(searchError); - }); - }), - abort, + return Promise.reject(searchError); + }), + abort: () => controller.abort(), failedSearchRequests, }; },