mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
* 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
This commit is contained in:
parent
9d7324f95e
commit
c3f749f896
19 changed files with 626 additions and 235 deletions
|
@ -116,9 +116,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,
|
||||
|
|
1
src/core/public/http/_import_objects.ndjson
Normal file
1
src/core/public/http/_import_objects.ndjson
Normal file
|
@ -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}
|
91
src/core/public/http/fetch.ts
Normal file
91
src/core/public/http/fetch.ts
Normal file
|
@ -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<HttpBody> {
|
||||
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 };
|
||||
};
|
|
@ -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<T, K> = Pick<T, Exclude<keyof T, K>>;
|
||||
|
||||
function createAbortable() {
|
||||
const abortController = new AbortController();
|
||||
const { signal, abort } = abortController;
|
||||
|
||||
return {
|
||||
signal,
|
||||
abort: abort.bind(abortController),
|
||||
};
|
||||
}
|
||||
|
||||
export function kfetchAbortable(
|
||||
fetchOptions?: Omit<KFetchOptions, 'signal'>,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<HttpSetup> = {
|
||||
addLoadingCount: jest.fn(),
|
||||
getLoadingCount$: jest.fn(),
|
||||
};
|
||||
return setupContract;
|
||||
};
|
||||
import { HttpService, HttpSetup, HttpStart } from './http_service';
|
||||
|
||||
type HttpServiceContract = PublicMethodsOf<HttpService>;
|
||||
const createMock = () => {
|
||||
const mocked: jest.Mocked<HttpServiceContract> = {
|
||||
setup: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
};
|
||||
mocked.setup.mockReturnValue(createSetupContractMock());
|
||||
return mocked;
|
||||
};
|
||||
const createSetupContractMock = (): jest.Mocked<HttpSetup> => ({
|
||||
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<HttpStart> => undefined;
|
||||
const createMock = (): jest.Mocked<PublicMethodsOf<HttpService>> => ({
|
||||
setup: jest.fn().mockReturnValue(createSetupContractMock()),
|
||||
start: jest.fn().mockReturnValue(createStartContractMock()),
|
||||
stop: jest.fn(),
|
||||
});
|
||||
|
||||
export const httpServiceMock = {
|
||||
create: createMock,
|
||||
|
|
|
@ -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<number>();
|
||||
const countB$ = new Rx.Subject<number>();
|
||||
const countC$ = new Rx.Subject<number>();
|
||||
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<number>();
|
||||
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();
|
||||
});
|
||||
|
|
|
@ -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<number>) => {
|
||||
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<HttpService['setup']>;
|
||||
export type HttpStart = ReturnType<HttpService['start']>;
|
||||
|
|
|
@ -18,3 +18,4 @@
|
|||
*/
|
||||
|
||||
export { HttpService, HttpSetup } from './http_service';
|
||||
export { HttpFetchError } from './http_fetch_error';
|
||||
|
|
55
src/core/public/http/types.ts
Normal file
55
src/core/public/http/types.ts
Normal file
|
@ -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;
|
|
@ -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);
|
||||
|
|
|
@ -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<jest.Mocked<typeof mockPersistedL
|
|||
export const mockGetAutocompleteSuggestions = 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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
1
src/legacy/ui/public/kfetch/_import_objects.ndjson
Normal file
1
src/legacy/ui/public/kfetch/_import_objects.ndjson
Normal file
|
@ -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}
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<any>) {
|
|||
}, 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;
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
38
src/test_utils/public/kfetch_test_setup.ts
Normal file
38
src/test_utils/public/kfetch_test_setup.ts
Normal file
|
@ -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 };
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue