Add asResponse option to HttpService methods (#52434)

This commit is contained in:
Josh Dover 2019-12-11 10:53:17 -06:00 committed by GitHub
parent ab1fe3f14e
commit a91e53f18f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 571 additions and 383 deletions

View file

@ -1,12 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpBody](./kibana-plugin-public.httpbody.md)
## HttpBody type
<b>Signature:</b>
```typescript
export declare type HttpBody = BodyInit | null | any;
```

View file

@ -8,7 +8,7 @@
<b>Signature:</b>
```typescript
export interface HttpErrorResponse extends HttpResponse
export interface HttpErrorResponse extends IHttpResponse
```
## Properties

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) &gt; [asResponse](./kibana-plugin-public.httpfetchoptions.asresponse.md)
## HttpFetchOptions.asResponse property
When `true` the return type of [HttpHandler](./kibana-plugin-public.httphandler.md) will be an [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) with detailed request and response information. When `false`<!-- -->, the return type will just be the parsed response body. Defaults to `false`<!-- -->.
<b>Signature:</b>
```typescript
asResponse?: boolean;
```

View file

@ -16,6 +16,7 @@ export interface HttpFetchOptions extends HttpRequestInit
| Property | Type | Description |
| --- | --- | --- |
| [asResponse](./kibana-plugin-public.httpfetchoptions.asresponse.md) | <code>boolean</code> | When <code>true</code> the return type of [HttpHandler](./kibana-plugin-public.httphandler.md) will be an [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) with detailed request and response information. When <code>false</code>, the return type will just be the parsed response body. Defaults to <code>false</code>. |
| [headers](./kibana-plugin-public.httpfetchoptions.headers.md) | <code>HttpHeadersInit</code> | Headers to send with the request. See [HttpHeadersInit](./kibana-plugin-public.httpheadersinit.md)<!-- -->. |
| [prependBasePath](./kibana-plugin-public.httpfetchoptions.prependbasepath.md) | <code>boolean</code> | Whether or not the request should automatically prepend the basePath. Defaults to <code>true</code>. |
| [query](./kibana-plugin-public.httpfetchoptions.query.md) | <code>HttpFetchQuery</code> | The query string for an HTTP request. See [HttpFetchQuery](./kibana-plugin-public.httpfetchquery.md)<!-- -->. |

View file

@ -2,12 +2,12 @@
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpHandler](./kibana-plugin-public.httphandler.md)
## HttpHandler type
## HttpHandler interface
A function for making an HTTP requests to Kibana's backend. See [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) for options and [HttpBody](./kibana-plugin-public.httpbody.md) for the response.
A function for making an HTTP requests to Kibana's backend. See [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) for options and [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) for the response.
<b>Signature:</b>
```typescript
export declare type HttpHandler = (path: string, options?: HttpFetchOptions) => Promise<HttpBody>;
export interface HttpHandler
```

View file

@ -9,17 +9,17 @@ Define an interceptor to be executed after a response is received.
<b>Signature:</b>
```typescript
response?(httpResponse: HttpResponse, controller: IHttpInterceptController): Promise<InterceptedHttpResponse> | InterceptedHttpResponse | void;
response?(httpResponse: IHttpResponse, controller: IHttpInterceptController): Promise<IHttpResponseInterceptorOverrides> | IHttpResponseInterceptorOverrides | void;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| httpResponse | <code>HttpResponse</code> | |
| httpResponse | <code>IHttpResponse</code> | |
| controller | <code>IHttpInterceptController</code> | |
<b>Returns:</b>
`Promise<InterceptedHttpResponse> | InterceptedHttpResponse | void`
`Promise<IHttpResponseInterceptorOverrides> | IHttpResponseInterceptorOverrides | void`

View file

@ -9,7 +9,7 @@ Define an interceptor to be executed if a response interceptor throws an error o
<b>Signature:</b>
```typescript
responseError?(httpErrorResponse: HttpErrorResponse, controller: IHttpInterceptController): Promise<InterceptedHttpResponse> | InterceptedHttpResponse | void;
responseError?(httpErrorResponse: HttpErrorResponse, controller: IHttpInterceptController): Promise<IHttpResponseInterceptorOverrides> | IHttpResponseInterceptorOverrides | void;
```
## Parameters
@ -21,5 +21,5 @@ responseError?(httpErrorResponse: HttpErrorResponse, controller: IHttpInterceptC
<b>Returns:</b>
`Promise<InterceptedHttpResponse> | InterceptedHttpResponse | void`
`Promise<IHttpResponseInterceptorOverrides> | IHttpResponseInterceptorOverrides | void`

View file

@ -1,19 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpResponse](./kibana-plugin-public.httpresponse.md)
## HttpResponse interface
<b>Signature:</b>
```typescript
export interface HttpResponse extends InterceptedHttpResponse
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [request](./kibana-plugin-public.httpresponse.request.md) | <code>Readonly&lt;Request&gt;</code> | |

View file

@ -1,11 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpResponse](./kibana-plugin-public.httpresponse.md) &gt; [request](./kibana-plugin-public.httpresponse.request.md)
## HttpResponse.request property
<b>Signature:</b>
```typescript
request: Readonly<Request>;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) &gt; [body](./kibana-plugin-public.ihttpresponse.body.md)
## IHttpResponse.body property
Parsed body received, may be undefined if there was an error.
<b>Signature:</b>
```typescript
readonly body?: TResponseBody;
```

View file

@ -0,0 +1,21 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [IHttpResponse](./kibana-plugin-public.ihttpresponse.md)
## IHttpResponse interface
<b>Signature:</b>
```typescript
export interface IHttpResponse<TResponseBody = any>
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [body](./kibana-plugin-public.ihttpresponse.body.md) | <code>TResponseBody</code> | Parsed body received, may be undefined if there was an error. |
| [request](./kibana-plugin-public.ihttpresponse.request.md) | <code>Readonly&lt;Request&gt;</code> | Raw request sent to Kibana server. |
| [response](./kibana-plugin-public.ihttpresponse.response.md) | <code>Readonly&lt;Response&gt;</code> | Raw response received, may be undefined if there was an error. |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) &gt; [request](./kibana-plugin-public.ihttpresponse.request.md)
## IHttpResponse.request property
Raw request sent to Kibana server.
<b>Signature:</b>
```typescript
readonly request: Readonly<Request>;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) &gt; [response](./kibana-plugin-public.ihttpresponse.response.md)
## IHttpResponse.response property
Raw response received, may be undefined if there was an error.
<b>Signature:</b>
```typescript
readonly response?: Readonly<Response>;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [IHttpResponseInterceptorOverrides](./kibana-plugin-public.ihttpresponseinterceptoroverrides.md) &gt; [body](./kibana-plugin-public.ihttpresponseinterceptoroverrides.body.md)
## IHttpResponseInterceptorOverrides.body property
Parsed body received, may be undefined if there was an error.
<b>Signature:</b>
```typescript
readonly body?: TResponseBody;
```

View file

@ -0,0 +1,21 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [IHttpResponseInterceptorOverrides](./kibana-plugin-public.ihttpresponseinterceptoroverrides.md)
## IHttpResponseInterceptorOverrides interface
Properties that can be returned by HttpInterceptor.request to override the response.
<b>Signature:</b>
```typescript
export interface IHttpResponseInterceptorOverrides<TResponseBody = any>
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [body](./kibana-plugin-public.ihttpresponseinterceptoroverrides.body.md) | <code>TResponseBody</code> | Parsed body received, may be undefined if there was an error. |
| [response](./kibana-plugin-public.ihttpresponseinterceptoroverrides.response.md) | <code>Readonly&lt;Response&gt;</code> | Raw response received, may be undefined if there was an error. |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [IHttpResponseInterceptorOverrides](./kibana-plugin-public.ihttpresponseinterceptoroverrides.md) &gt; [response](./kibana-plugin-public.ihttpresponseinterceptoroverrides.response.md)
## IHttpResponseInterceptorOverrides.response property
Raw response received, may be undefined if there was an error.
<b>Signature:</b>
```typescript
readonly response?: Readonly<Response>;
```

View file

@ -1,11 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [InterceptedHttpResponse](./kibana-plugin-public.interceptedhttpresponse.md) &gt; [body](./kibana-plugin-public.interceptedhttpresponse.body.md)
## InterceptedHttpResponse.body property
<b>Signature:</b>
```typescript
body?: HttpBody;
```

View file

@ -1,20 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [InterceptedHttpResponse](./kibana-plugin-public.interceptedhttpresponse.md)
## InterceptedHttpResponse interface
<b>Signature:</b>
```typescript
export interface InterceptedHttpResponse
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [body](./kibana-plugin-public.interceptedhttpresponse.body.md) | <code>HttpBody</code> | |
| [response](./kibana-plugin-public.interceptedhttpresponse.response.md) | <code>Response</code> | |

View file

@ -1,11 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [InterceptedHttpResponse](./kibana-plugin-public.interceptedhttpresponse.md) &gt; [response](./kibana-plugin-public.interceptedhttpresponse.response.md)
## InterceptedHttpResponse.response property
<b>Signature:</b>
```typescript
response?: Response;
```

View file

@ -52,10 +52,10 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [HttpErrorResponse](./kibana-plugin-public.httperrorresponse.md) | |
| [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) | All options that may be used with a [HttpHandler](./kibana-plugin-public.httphandler.md)<!-- -->. |
| [HttpFetchQuery](./kibana-plugin-public.httpfetchquery.md) | |
| [HttpHandler](./kibana-plugin-public.httphandler.md) | A function for making an HTTP requests to Kibana's backend. See [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) for options and [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) for the response. |
| [HttpHeadersInit](./kibana-plugin-public.httpheadersinit.md) | |
| [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md) | An object that may define global interceptor functions for different parts of the request and response lifecycle. See [IHttpInterceptController](./kibana-plugin-public.ihttpinterceptcontroller.md)<!-- -->. |
| [HttpRequestInit](./kibana-plugin-public.httprequestinit.md) | Fetch API options available to [HttpHandler](./kibana-plugin-public.httphandler.md)<!-- -->s. |
| [HttpResponse](./kibana-plugin-public.httpresponse.md) | |
| [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) | |
| [I18nStart](./kibana-plugin-public.i18nstart.md) | I18nStart.Context is required by any localizable React component from @<!-- -->kbn/i18n and @<!-- -->elastic/eui packages and is supposed to be used as the topmost component for any i18n-compatible React tree. |
| [IAnonymousPaths](./kibana-plugin-public.ianonymouspaths.md) | APIs for denoting paths as not requiring authentication |
@ -63,7 +63,8 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [IContextContainer](./kibana-plugin-public.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. |
| [IHttpFetchError](./kibana-plugin-public.ihttpfetcherror.md) | |
| [IHttpInterceptController](./kibana-plugin-public.ihttpinterceptcontroller.md) | Used to halt a request Promise chain in a [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md)<!-- -->. |
| [InterceptedHttpResponse](./kibana-plugin-public.interceptedhttpresponse.md) | |
| [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) | |
| [IHttpResponseInterceptorOverrides](./kibana-plugin-public.ihttpresponseinterceptoroverrides.md) | Properties that can be returned by HttpInterceptor.request to override the response. |
| [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) | Client-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) |
| [LegacyCoreSetup](./kibana-plugin-public.legacycoresetup.md) | Setup interface exposed to the legacy platform via the <code>ui/new_platform</code> module. |
| [LegacyCoreStart](./kibana-plugin-public.legacycorestart.md) | Start interface exposed to the legacy platform via the <code>ui/new_platform</code> module. |
@ -108,8 +109,6 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [HandlerContextType](./kibana-plugin-public.handlercontexttype.md) | Extracts the type of the first argument of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md) to represent the type of the context. |
| [HandlerFunction](./kibana-plugin-public.handlerfunction.md) | A function that accepts a context object and an optional number of additional arguments. Used for the generic types in [IContextContainer](./kibana-plugin-public.icontextcontainer.md) |
| [HandlerParameters](./kibana-plugin-public.handlerparameters.md) | Extracts the types of the additional arguments of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md)<!-- -->, excluding the [HandlerContextType](./kibana-plugin-public.handlercontexttype.md)<!-- -->. |
| [HttpBody](./kibana-plugin-public.httpbody.md) | |
| [HttpHandler](./kibana-plugin-public.httphandler.md) | A function for making an HTTP requests to Kibana's backend. See [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) for options and [HttpBody](./kibana-plugin-public.httpbody.md) for the response. |
| [HttpSetup](./kibana-plugin-public.httpsetup.md) | See [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) |
| [HttpStart](./kibana-plugin-public.httpstart.md) | See [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) |
| [IContextProvider](./kibana-plugin-public.icontextprovider.md) | A function that returns a context value for a specific key of given context type. |

View file

@ -74,7 +74,7 @@ export class CapabilitiesService {
const url = http.anonymousPaths.isAnonymous(window.location.pathname)
? '/api/core/capabilities/defaults'
: '/api/core/capabilities';
const capabilities = await http.post(url, {
const capabilities = await http.post<Capabilities>(url, {
body: payload,
});
return deepFreeze(capabilities);

View file

@ -0,0 +1,155 @@
/*
* 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 { IBasePath, HttpInterceptor, HttpHandler, HttpFetchOptions, IHttpResponse } from './types';
import { HttpFetchError } from './http_fetch_error';
import { HttpInterceptController } from './http_intercept_controller';
import { HttpResponse } from './response';
import { interceptRequest, interceptResponse } from './intercept';
import { HttpInterceptHaltError } from './http_intercept_halt_error';
interface Params {
basePath: IBasePath;
kibanaVersion: string;
}
const JSON_CONTENT = /^(application\/(json|x-javascript)|text\/(x-)?javascript|x-json)(;.*)?$/;
const NDJSON_CONTENT = /^(application\/ndjson)(;.*)?$/;
export class FetchService {
private readonly interceptors = new Set<HttpInterceptor>();
constructor(private readonly params: Params) {}
public intercept(interceptor: HttpInterceptor) {
this.interceptors.add(interceptor);
return () => this.interceptors.delete(interceptor);
}
public removeAllInterceptors() {
this.interceptors.clear();
}
public fetch: HttpHandler = async <TResponseBody>(
path: string,
options: HttpFetchOptions = {}
) => {
const initialRequest = this.createRequest(path, options);
const controller = new HttpInterceptController();
// We wrap the interception in a separate promise to ensure that when
// a halt is called we do not resolve or reject, halting handling of the promise.
return new Promise<TResponseBody | IHttpResponse<TResponseBody>>(async (resolve, reject) => {
try {
const interceptedRequest = await interceptRequest(
initialRequest,
this.interceptors,
controller
);
const initialResponse = this.fetchResponse(interceptedRequest);
const interceptedResponse = await interceptResponse(
initialResponse,
this.interceptors,
controller
);
if (options.asResponse) {
resolve(interceptedResponse);
} else {
resolve(interceptedResponse.body);
}
} catch (error) {
if (!(error instanceof HttpInterceptHaltError)) {
reject(error);
}
}
});
};
private createRequest(path: string, options?: HttpFetchOptions): Request {
// Merge and destructure options out that are not applicable to the Fetch API.
const { query, prependBasePath: shouldPrependBasePath, asResponse, ...fetchOptions } = merge(
{
method: 'GET',
credentials: 'same-origin',
prependBasePath: true,
headers: {
'kbn-version': this.params.kibanaVersion,
'Content-Type': 'application/json',
},
},
options || {}
);
const url = format({
pathname: shouldPrependBasePath ? this.params.basePath.prepend(path) : path,
query,
});
if (
options &&
options.headers &&
'Content-Type' in options.headers &&
options.headers['Content-Type'] === undefined
) {
delete fetchOptions.headers['Content-Type'];
}
return new Request(url, fetchOptions);
}
private async fetchResponse(request: Request) {
let response: Response;
let body = null;
try {
response = await window.fetch(request);
} catch (err) {
throw new HttpFetchError(err.message, request);
}
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 {
const text = await response.text();
try {
body = JSON.parse(text);
} catch (err) {
body = text;
}
}
} catch (err) {
throw new HttpFetchError(err.message, request, response, body);
}
if (!response.ok) {
throw new HttpFetchError(response.statusText, request, response, body);
}
return new HttpResponse({ request, response, body });
}
}

View file

@ -24,7 +24,7 @@ import fetchMock from 'fetch-mock/es5/client';
import { readFileSync } from 'fs';
import { join } from 'path';
import { setup, SetupTap } from '../../../test_utils/public/http_test_setup';
import { HttpResponse } from './types';
import { IHttpResponse } from './types';
function delay<T>(duration: number) {
return new Promise<T>(r => setTimeout(r, duration));
@ -101,32 +101,32 @@ describe('http requests', () => {
it('should return response', async () => {
const { http } = setup();
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 } = setup();
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 } = setup();
fetchMock.get('*', {});
await http.fetch('my/path', { prependBasePath: false });
expect(fetchMock.lastUrl()).toBe('/my/path');
});
it('should not include undefined query params', async () => {
const { http } = setup();
fetchMock.get('*', {});
await http.fetch('/my/path', { query: { a: undefined } });
expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path');
});
it('should make request with defaults', async () => {
const { http } = setup();
@ -145,6 +145,18 @@ describe('http requests', () => {
});
});
it('should expose detailed response object when asResponse = true', async () => {
const { http } = setup();
fetchMock.get('*', { foo: 'bar' });
const response = await http.fetch('/my/path', { asResponse: true });
expect(response.request).toBeInstanceOf(Request);
expect(response.response).toBeInstanceOf(Response);
expect(response.body).toEqual({ foo: 'bar' });
});
it('should reject on network error', async () => {
const { http } = setup();
@ -496,7 +508,7 @@ describe('interception', () => {
it('should accumulate response information', async () => {
const bodies = ['alpha', 'beta', 'gamma'];
const createResponse = jest.fn((httpResponse: HttpResponse) => ({
const createResponse = jest.fn((httpResponse: IHttpResponse) => ({
body: bodies.shift(),
}));

View file

@ -27,21 +27,16 @@ import {
takeUntil,
tap,
} from 'rxjs/operators';
import { merge } from 'lodash';
import { format } from 'url';
import { InjectedMetadataSetup } from '../injected_metadata';
import { FatalErrorsSetup } from '../fatal_errors';
import { HttpFetchOptions, HttpServiceBase, HttpInterceptor, HttpResponse } from './types';
import { HttpFetchOptions, HttpServiceBase } from './types';
import { HttpInterceptController } from './http_intercept_controller';
import { HttpFetchError } from './http_fetch_error';
import { HttpInterceptHaltError } from './http_intercept_halt_error';
import { BasePath } from './base_path_service';
import { AnonymousPaths } from './anonymous_paths';
import { FetchService } from './fetch';
const JSON_CONTENT = /^(application\/(json|x-javascript)|text\/(x-)?javascript|x-json)(;.*)?$/;
const NDJSON_CONTENT = /^(application\/ndjson)(;.*)?$/;
function checkHalt(controller: HttpInterceptController, error?: Error) {
export function checkHalt(controller: HttpInterceptController, error?: Error) {
if (error instanceof HttpInterceptHaltError) {
throw error;
} else if (controller.halted) {
@ -55,224 +50,15 @@ export const setup = (
): HttpServiceBase => {
const loadingCount$ = new BehaviorSubject(0);
const stop$ = new Subject();
const interceptors = new Set<HttpInterceptor>();
const kibanaVersion = injectedMetadata.getKibanaVersion();
const basePath = new BasePath(injectedMetadata.getBasePath());
const anonymousPaths = new AnonymousPaths(basePath);
function intercept(interceptor: HttpInterceptor) {
interceptors.add(interceptor);
return () => interceptors.delete(interceptor);
}
function removeAllInterceptors() {
interceptors.clear();
}
function createRequest(path: string, options?: HttpFetchOptions) {
const { query, prependBasePath: shouldPrependBasePath, ...fetchOptions } = merge(
{
method: 'GET',
credentials: 'same-origin',
prependBasePath: true,
headers: {
'kbn-version': kibanaVersion,
'Content-Type': 'application/json',
},
},
options || {}
);
const url = format({
pathname: shouldPrependBasePath ? basePath.prepend(path) : path,
query,
});
if (
options &&
options.headers &&
'Content-Type' in options.headers &&
options.headers['Content-Type'] === undefined
) {
delete fetchOptions.headers['Content-Type'];
}
return new Request(url, fetchOptions);
}
// Request/response interceptors are called in opposite orders.
// Request hooks start from the newest interceptor and end with the oldest.
function interceptRequest(
request: Request,
controller: HttpInterceptController
): Promise<Request> {
let next = request;
return [...interceptors].reduceRight(
(promise, interceptor) =>
promise.then(
async (current: Request) => {
next = current;
checkHalt(controller);
if (!interceptor.request) {
return current;
}
return (await interceptor.request(current, controller)) || current;
},
async error => {
checkHalt(controller, error);
if (!interceptor.requestError) {
throw error;
}
const nextRequest = await interceptor.requestError(
{ error, request: next },
controller
);
if (!nextRequest) {
throw error;
}
next = nextRequest;
return next;
}
),
Promise.resolve(request)
);
}
// Response hooks start from the oldest interceptor and end with the newest.
async function interceptResponse(
responsePromise: Promise<HttpResponse>,
controller: HttpInterceptController
) {
let current: HttpResponse | undefined;
const finalHttpResponse = await [...interceptors].reduce(
(promise, interceptor) =>
promise.then(
async httpResponse => {
current = httpResponse;
checkHalt(controller);
if (!interceptor.response) {
return httpResponse;
}
return {
...httpResponse,
...((await interceptor.response(httpResponse, controller)) || {}),
};
},
async error => {
const request = error.request || (current && current.request);
checkHalt(controller, error);
if (!interceptor.responseError) {
throw error;
}
try {
const next = await interceptor.responseError(
{
error,
request,
response: error.response || (current && current.response),
body: error.body || (current && current.body),
},
controller
);
checkHalt(controller, error);
if (!next) {
throw error;
}
return { ...next, request };
} catch (err) {
checkHalt(controller, err);
throw err;
}
}
),
responsePromise
);
return finalHttpResponse.body;
}
async function fetcher(request: Request): Promise<HttpResponse> {
let response;
let body = null;
try {
response = await window.fetch(request);
} catch (err) {
throw new HttpFetchError(err.message, request);
}
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 {
const text = await response.text();
try {
body = JSON.parse(text);
} catch (err) {
body = text;
}
}
} catch (err) {
throw new HttpFetchError(err.message, request, response, body);
}
if (!response.ok) {
throw new HttpFetchError(response.statusText, request, response, body);
}
return { response, body, request };
}
async function fetch(path: string, options: HttpFetchOptions = {}) {
const controller = new HttpInterceptController();
const initialRequest = createRequest(path, options);
// We wrap the interception in a separate promise to ensure that when
// a halt is called we do not resolve or reject, halting handling of the promise.
return new Promise(async (resolve, reject) => {
function rejectIfNotHalted(err: any) {
if (!(err instanceof HttpInterceptHaltError)) {
reject(err);
}
}
try {
const request = await interceptRequest(initialRequest, controller);
try {
resolve(await interceptResponse(fetcher(request), controller));
} catch (err) {
rejectIfNotHalted(err);
}
} catch (err) {
rejectIfNotHalted(err);
}
});
}
const fetchService = new FetchService({ basePath, kibanaVersion });
function shorthand(method: string) {
return (path: string, options: HttpFetchOptions = {}) => fetch(path, { ...options, method });
return (path: string, options: HttpFetchOptions = {}) =>
fetchService.fetch(path, { ...options, method });
}
function stop() {
@ -321,9 +107,9 @@ export const setup = (
stop,
basePath,
anonymousPaths,
intercept,
removeAllInterceptors,
fetch,
intercept: fetchService.intercept.bind(fetchService),
removeAllInterceptors: fetchService.removeAllInterceptors.bind(fetchService),
fetch: fetchService.fetch.bind(fetchService),
delete: shorthand('DELETE'),
get: shorthand('GET'),
head: shorthand('HEAD'),

View file

@ -0,0 +1,134 @@
/*
* 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 { HttpInterceptController } from './http_intercept_controller';
import { HttpInterceptHaltError } from './http_intercept_halt_error';
import { HttpInterceptor, IHttpResponse } from './types';
import { HttpResponse } from './response';
export async function interceptRequest(
request: Request,
interceptors: ReadonlySet<HttpInterceptor>,
controller: HttpInterceptController
): Promise<Request> {
let next = request;
return [...interceptors].reduceRight(
(promise, interceptor) =>
promise.then(
async (current: Request) => {
next = current;
checkHalt(controller);
if (!interceptor.request) {
return current;
}
return (await interceptor.request(current, controller)) || current;
},
async error => {
checkHalt(controller, error);
if (!interceptor.requestError) {
throw error;
}
const nextRequest = await interceptor.requestError({ error, request: next }, controller);
if (!nextRequest) {
throw error;
}
next = nextRequest;
return next;
}
),
Promise.resolve(request)
);
}
export async function interceptResponse(
responsePromise: Promise<IHttpResponse>,
interceptors: ReadonlySet<HttpInterceptor>,
controller: HttpInterceptController
): Promise<IHttpResponse> {
let current: IHttpResponse;
return await [...interceptors].reduce(
(promise, interceptor) =>
promise.then(
async httpResponse => {
current = httpResponse;
checkHalt(controller);
if (!interceptor.response) {
return httpResponse;
}
const interceptorOverrides = (await interceptor.response(httpResponse, controller)) || {};
return new HttpResponse({
...httpResponse,
...interceptorOverrides,
});
},
async error => {
const request = error.request || (current && current.request);
checkHalt(controller, error);
if (!interceptor.responseError) {
throw error;
}
try {
const next = await interceptor.responseError(
{
error,
request,
response: error.response || (current && current.response),
body: error.body || (current && current.body),
},
controller
);
checkHalt(controller, error);
if (!next) {
throw error;
}
return new HttpResponse({ ...next, request });
} catch (err) {
checkHalt(controller, err);
throw err;
}
}
),
responsePromise
);
}
function checkHalt(controller: HttpInterceptController, error?: Error) {
if (error instanceof HttpInterceptHaltError) {
throw error;
} else if (controller.halted) {
throw new HttpInterceptHaltError();
}
}

View file

@ -0,0 +1,40 @@
/*
* 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 { IHttpResponse } from './types';
export class HttpResponse<TResponseBody = any> implements IHttpResponse<TResponseBody> {
public readonly request: Request;
public readonly response?: Response;
public readonly body?: TResponseBody;
constructor({
request,
response,
body,
}: {
request: Request;
response?: Response;
body?: TResponseBody;
}) {
this.request = request;
this.response = response;
this.body = body;
}
}

View file

@ -229,31 +229,49 @@ export interface HttpFetchOptions extends HttpRequestInit {
* Headers to send with the request. See {@link HttpHeadersInit}.
*/
headers?: HttpHeadersInit;
/**
* When `true` the return type of {@link HttpHandler} will be an {@link IHttpResponse} with detailed request and
* response information. When `false`, the return type will just be the parsed response body. Defaults to `false`.
*/
asResponse?: boolean;
}
/**
* A function for making an HTTP requests to Kibana's backend. See {@link HttpFetchOptions} for options and
* {@link HttpBody} for the response.
* {@link IHttpResponse} for the response.
*
* @param path the path on the Kibana server to send the request to. Should not include the basePath.
* @param options {@link HttpFetchOptions}
* @returns a Promise that resolves to a {@link HttpBody}
* @returns a Promise that resolves to a {@link IHttpResponse}
* @public
*/
export type HttpHandler = (path: string, options?: HttpFetchOptions) => Promise<HttpBody>;
/** @public */
export type HttpBody = BodyInit | null | any;
/** @public */
export interface InterceptedHttpResponse {
response?: Response;
body?: HttpBody;
export interface HttpHandler {
<TResponseBody = any>(path: string, options: HttpFetchOptions & { asResponse: true }): Promise<
IHttpResponse<TResponseBody>
>;
<TResponseBody = any>(path: string, options?: HttpFetchOptions): Promise<TResponseBody>;
}
/** @public */
export interface HttpResponse extends InterceptedHttpResponse {
request: Readonly<Request>;
export interface IHttpResponse<TResponseBody = any> {
/** Raw request sent to Kibana server. */
readonly request: Readonly<Request>;
/** Raw response received, may be undefined if there was an error. */
readonly response?: Readonly<Response>;
/** Parsed body received, may be undefined if there was an error. */
readonly body?: TResponseBody;
}
/**
* Properties that can be returned by HttpInterceptor.request to override the response.
* @public
*/
export interface IHttpResponseInterceptorOverrides<TResponseBody = any> {
/** Raw response received, may be undefined if there was an error. */
readonly response?: Readonly<Response>;
/** Parsed body received, may be undefined if there was an error. */
readonly body?: TResponseBody;
}
/** @public */
@ -272,7 +290,7 @@ export interface IHttpFetchError extends Error {
}
/** @public */
export interface HttpErrorResponse extends HttpResponse {
export interface HttpErrorResponse extends IHttpResponse {
error: Error | IHttpFetchError;
}
/** @public */
@ -310,13 +328,13 @@ export interface HttpInterceptor {
/**
* Define an interceptor to be executed after a response is received.
* @param httpResponse {@link HttpResponse}
* @param httpResponse {@link IHttpResponse}
* @param controller {@link IHttpInterceptController}
*/
response?(
httpResponse: HttpResponse,
httpResponse: IHttpResponse,
controller: IHttpInterceptController
): Promise<InterceptedHttpResponse> | InterceptedHttpResponse | void;
): Promise<IHttpResponseInterceptorOverrides> | IHttpResponseInterceptorOverrides | void;
/**
* Define an interceptor to be executed if a response interceptor throws an error or returns a rejected Promise.
@ -326,7 +344,7 @@ export interface HttpInterceptor {
responseError?(
httpErrorResponse: HttpErrorResponse,
controller: IHttpInterceptController
): Promise<InterceptedHttpResponse> | InterceptedHttpResponse | void;
): Promise<IHttpResponseInterceptorOverrides> | IHttpResponseInterceptorOverrides | void;
}
/**

View file

@ -112,14 +112,13 @@ export {
HttpErrorResponse,
HttpErrorRequest,
HttpInterceptor,
HttpResponse,
IHttpResponse,
HttpHandler,
HttpBody,
IBasePath,
IAnonymousPaths,
IHttpInterceptController,
IHttpFetchError,
InterceptedHttpResponse,
IHttpResponseInterceptorOverrides,
} from './http';
export { OverlayStart, OverlayBannersStart, OverlayRef } from './overlays';

View file

@ -464,9 +464,6 @@ export type HandlerFunction<T extends object> = (context: T, ...args: any[]) =>
// @public
export type HandlerParameters<T extends HandlerFunction<any>> = T extends (context: any, ...args: infer U) => any ? U : never;
// @public (undocumented)
export type HttpBody = BodyInit | null | any;
// @public (undocumented)
export interface HttpErrorRequest {
// (undocumented)
@ -476,13 +473,14 @@ export interface HttpErrorRequest {
}
// @public (undocumented)
export interface HttpErrorResponse extends HttpResponse {
export interface HttpErrorResponse extends IHttpResponse {
// (undocumented)
error: Error | IHttpFetchError;
}
// @public
export interface HttpFetchOptions extends HttpRequestInit {
asResponse?: boolean;
headers?: HttpHeadersInit;
prependBasePath?: boolean;
query?: HttpFetchQuery;
@ -495,7 +493,14 @@ export interface HttpFetchQuery {
}
// @public
export type HttpHandler = (path: string, options?: HttpFetchOptions) => Promise<HttpBody>;
export interface HttpHandler {
// (undocumented)
<TResponseBody = any>(path: string, options: HttpFetchOptions & {
asResponse: true;
}): Promise<IHttpResponse<TResponseBody>>;
// (undocumented)
<TResponseBody = any>(path: string, options?: HttpFetchOptions): Promise<TResponseBody>;
}
// @public (undocumented)
export interface HttpHeadersInit {
@ -507,8 +512,8 @@ export interface HttpHeadersInit {
export interface HttpInterceptor {
request?(request: Request, controller: IHttpInterceptController): Promise<Request> | Request | void;
requestError?(httpErrorRequest: HttpErrorRequest, controller: IHttpInterceptController): Promise<Request> | Request | void;
response?(httpResponse: HttpResponse, controller: IHttpInterceptController): Promise<InterceptedHttpResponse> | InterceptedHttpResponse | void;
responseError?(httpErrorResponse: HttpErrorResponse, controller: IHttpInterceptController): Promise<InterceptedHttpResponse> | InterceptedHttpResponse | void;
response?(httpResponse: IHttpResponse, controller: IHttpInterceptController): Promise<IHttpResponseInterceptorOverrides> | IHttpResponseInterceptorOverrides | void;
responseError?(httpErrorResponse: HttpErrorResponse, controller: IHttpInterceptController): Promise<IHttpResponseInterceptorOverrides> | IHttpResponseInterceptorOverrides | void;
}
// @public
@ -529,12 +534,6 @@ export interface HttpRequestInit {
window?: null;
}
// @public (undocumented)
export interface HttpResponse extends InterceptedHttpResponse {
// (undocumented)
request: Readonly<Request>;
}
// @public (undocumented)
export interface HttpServiceBase {
addLoadingCount(countSource$: Observable<number>): void;
@ -613,11 +612,16 @@ export interface IHttpInterceptController {
}
// @public (undocumented)
export interface InterceptedHttpResponse {
// (undocumented)
body?: HttpBody;
// (undocumented)
response?: Response;
export interface IHttpResponse<TResponseBody = any> {
readonly body?: TResponseBody;
readonly request: Readonly<Request>;
readonly response?: Readonly<Response>;
}
// @public
export interface IHttpResponseInterceptorOverrides<TResponseBody = any> {
readonly body?: TResponseBody;
readonly response?: Readonly<Response>;
}
// @public

View file

@ -34,7 +34,7 @@ const defaultProps = {
isDirty: false,
onClose: () => {},
basePath: '',
post: () => Promise.resolve(),
post: () => Promise.resolve({} as any),
objectType: 'dashboard',
};

View file

@ -28,7 +28,7 @@ const defaultProps = {
allowShortUrl: true,
objectType: 'dashboard',
basePath: '',
post: () => Promise.resolve(),
post: () => Promise.resolve({} as any),
};
test('render', () => {

View file

@ -12,7 +12,7 @@ const icon = getSuitableIcon('');
describe('fetch_top_nodes', () => {
it('should build terms agg', async () => {
const postMock = jest.fn(() => Promise.resolve({ resp: {} }));
await fetchTopNodes(postMock, 'test', [
await fetchTopNodes(postMock as any, 'test', [
{ color: '', hopSize: 5, icon, name: 'field1', selected: false, type: 'string' },
{ color: '', hopSize: 5, icon, name: 'field2', selected: false, type: 'string' },
]);
@ -64,7 +64,7 @@ describe('fetch_top_nodes', () => {
},
})
);
const result = await fetchTopNodes(postMock, 'test', [
const result = await fetchTopNodes(postMock as any, 'test', [
{ color: 'red', hopSize: 5, icon, name: 'field1', selected: false, type: 'string' },
{ color: 'blue', hopSize: 5, icon, name: 'field2', selected: false, type: 'string' },
]);

View file

@ -278,7 +278,7 @@ describe('IndexPattern Data Panel', () => {
function testProps() {
const setState = jest.fn();
core.http.get = jest.fn(async (url: string) => {
core.http.get.mockImplementation(async (url: string) => {
const parts = url.split('/');
const indexPatternTitle = parts[parts.length - 1];
return {
@ -484,7 +484,7 @@ describe('IndexPattern Data Panel', () => {
let overlapCount = 0;
const props = testProps();
core.http.get = jest.fn((url: string) => {
core.http.get.mockImplementation((url: string) => {
if (queryCount) {
++overlapCount;
}
@ -533,11 +533,9 @@ describe('IndexPattern Data Panel', () => {
it('shows all fields if empty state button is clicked', async () => {
const props = testProps();
core.http.get = jest.fn((url: string) => {
return Promise.resolve({
indexPatternTitle: props.currentIndexPatternId,
existingFieldNames: [],
});
core.http.get.mockResolvedValue({
indexPatternTitle: props.currentIndexPatternId,
existingFieldNames: [],
});
const inst = mountWithIntl(<IndexPatternDataPanel {...props} />);

View file

@ -528,7 +528,8 @@ describe('loader', () => {
await syncExistingFields({
dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' },
fetchJson,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fetchJson: fetchJson as any,
indexPatterns: [{ title: 'a' }, { title: 'b' }, { title: 'c' }],
setState,
});

View file

@ -4,7 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { HttpInterceptor, HttpErrorResponse, HttpResponse, IAnonymousPaths } from 'src/core/public';
import {
HttpInterceptor,
HttpErrorResponse,
IHttpResponse,
IAnonymousPaths,
} from 'src/core/public';
import { ISessionTimeout } from './session_timeout';
@ -15,7 +20,7 @@ const isSystemAPIRequest = (request: Request) => {
export class SessionTimeoutHttpInterceptor implements HttpInterceptor {
constructor(private sessionTimeout: ISessionTimeout, private anonymousPaths: IAnonymousPaths) {}
response(httpResponse: HttpResponse) {
response(httpResponse: IHttpResponse) {
if (this.anonymousPaths.isAnonymous(window.location.pathname)) {
return;
}