Skip to content

Commit

Permalink
Expose an HTTP-request browser client (elastic#35486)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
eliperelman committed May 9, 2019
1 parent bb9e123 commit 3ca1dc1
Show file tree
Hide file tree
Showing 19 changed files with 626 additions and 235 deletions.
7 changes: 5 additions & 2 deletions src/core/public/core_system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/core/public/http/_import_objects.ndjson
Original file line number Diff line number Diff line change
@@ -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 changes: 91 additions & 0 deletions src/core/public/http/fetch.ts
Original file line number Diff line number Diff line change
@@ -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 };
};
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
36 changes: 19 additions & 17 deletions src/core/public/http/http_service.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 3ca1dc1

Please sign in to comment.