From 22f0746d972ead1b97f5c45d92f9d86abe6383f4 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Fri, 24 Jan 2020 15:20:09 -0700 Subject: [PATCH] Add isSystemRequest support to Kibana Platform (#53734) --- ...na-plugin-public.httperrorrequest.error.md | 11 -- .../kibana-plugin-public.httperrorrequest.md | 20 -- ...-plugin-public.httperrorrequest.request.md | 11 -- ...a-plugin-public.httperrorresponse.error.md | 11 -- .../kibana-plugin-public.httperrorresponse.md | 19 -- ...ugin-public.httpfetchoptions.asresponse.md | 2 +- ...public.httpfetchoptions.assystemrequest.md | 13 ++ .../kibana-plugin-public.httpfetchoptions.md | 3 +- ...-plugin-public.httpfetchoptionswithpath.md | 20 ++ ...in-public.httpfetchoptionswithpath.path.md | 11 ++ .../kibana-plugin-public.httphandler.md | 2 +- .../kibana-plugin-public.httpheadersinit.md | 1 + .../kibana-plugin-public.httpinterceptor.md | 2 +- ...a-plugin-public.httpinterceptor.request.md | 6 +- ...gin-public.httpinterceptor.requesterror.md | 6 +- ...-plugin-public.httpinterceptor.response.md | 6 +- ...in-public.httpinterceptor.responseerror.md | 6 +- ...ublic.httpinterceptorrequesterror.error.md | 11 ++ ...ttpinterceptorrequesterror.fetchoptions.md | 11 ++ ...ugin-public.httpinterceptorrequesterror.md | 20 ++ ...blic.httpinterceptorresponseerror.error.md | 11 ++ ...gin-public.httpinterceptorresponseerror.md | 20 ++ ...ic.httpinterceptorresponseerror.request.md | 11 ++ ...kibana-plugin-public.httpresponse.body.md} | 4 +- ...plugin-public.httpresponse.fetchoptions.md | 13 ++ .../kibana-plugin-public.httpresponse.md | 22 +++ ...ana-plugin-public.httpresponse.request.md} | 4 +- ...na-plugin-public.httpresponse.response.md} | 4 +- .../kibana-plugin-public.ihttpresponse.md | 21 -- .../core/public/kibana-plugin-public.md | 11 +- ...in-server.kibanarequest.issystemrequest.md | 13 ++ .../kibana-plugin-server.kibanarequest.md | 1 + packages/kbn-utility-types/index.ts | 17 +- src/core/public/http/fetch.test.ts | 180 ++++++++++++++++-- src/core/public/http/fetch.ts | 104 +++++++--- src/core/public/http/intercept.ts | 51 +++-- src/core/public/http/response.ts | 40 ---- src/core/public/http/types.ts | 70 +++++-- src/core/public/index.ts | 7 +- src/core/public/public.api.md | 71 ++++--- src/core/server/http/router/request.test.ts | 51 +++++ src/core/server/http/router/request.ts | 9 + src/core/server/server.api.md | 1 + .../kibana/server/lib/system_api.js | 1 + src/legacy/ui/public/kfetch/kfetch.ts | 1 + .../search/sync_search_strategy.test.ts | 6 +- .../public/search/sync_search_strategy.ts | 14 +- .../plugins/core_plugin_b/public/plugin.tsx | 14 +- .../plugins/core_plugin_b/server/plugin.ts | 10 + .../core_plugins/server_plugins.ts | 10 + .../test_suites/core_plugins/ui_plugins.ts | 24 ++- .../indexpattern_plugin/datapanel.test.tsx | 20 +- .../public/indexpattern_plugin/loader.test.ts | 4 +- .../lens/public/indexpattern_plugin/loader.ts | 3 +- .../reporting/public/lib/job_queue_client.ts | 10 +- .../lib/__tests__/replace_injected_vars.js | 1 + .../plugins/licensing/public/plugin.test.ts | 6 +- x-pack/plugins/licensing/public/plugin.ts | 7 +- .../authentication/authentication_service.ts | 2 +- .../roles/edit_role/edit_role_page.test.tsx | 4 +- .../management/roles/roles_api_client.test.ts | 2 +- .../public/session/session_timeout.tsx | 3 +- .../session_timeout_http_interceptor.test.ts | 8 +- .../session_timeout_http_interceptor.ts | 16 +- .../unauthorized_response_http_interceptor.ts | 7 +- 65 files changed, 757 insertions(+), 344 deletions(-) delete mode 100644 docs/development/core/public/kibana-plugin-public.httperrorrequest.error.md delete mode 100644 docs/development/core/public/kibana-plugin-public.httperrorrequest.md delete mode 100644 docs/development/core/public/kibana-plugin-public.httperrorrequest.request.md delete mode 100644 docs/development/core/public/kibana-plugin-public.httperrorresponse.error.md delete mode 100644 docs/development/core/public/kibana-plugin-public.httperrorresponse.md create mode 100644 docs/development/core/public/kibana-plugin-public.httpfetchoptions.assystemrequest.md create mode 100644 docs/development/core/public/kibana-plugin-public.httpfetchoptionswithpath.md create mode 100644 docs/development/core/public/kibana-plugin-public.httpfetchoptionswithpath.path.md create mode 100644 docs/development/core/public/kibana-plugin-public.httpinterceptorrequesterror.error.md create mode 100644 docs/development/core/public/kibana-plugin-public.httpinterceptorrequesterror.fetchoptions.md create mode 100644 docs/development/core/public/kibana-plugin-public.httpinterceptorrequesterror.md create mode 100644 docs/development/core/public/kibana-plugin-public.httpinterceptorresponseerror.error.md create mode 100644 docs/development/core/public/kibana-plugin-public.httpinterceptorresponseerror.md create mode 100644 docs/development/core/public/kibana-plugin-public.httpinterceptorresponseerror.request.md rename docs/development/core/public/{kibana-plugin-public.ihttpresponse.body.md => kibana-plugin-public.httpresponse.body.md} (63%) create mode 100644 docs/development/core/public/kibana-plugin-public.httpresponse.fetchoptions.md create mode 100644 docs/development/core/public/kibana-plugin-public.httpresponse.md rename docs/development/core/public/{kibana-plugin-public.ihttpresponse.request.md => kibana-plugin-public.httpresponse.request.md} (60%) rename docs/development/core/public/{kibana-plugin-public.ihttpresponse.response.md => kibana-plugin-public.httpresponse.response.md} (62%) delete mode 100644 docs/development/core/public/kibana-plugin-public.ihttpresponse.md create mode 100644 docs/development/core/server/kibana-plugin-server.kibanarequest.issystemrequest.md delete mode 100644 src/core/public/http/response.ts diff --git a/docs/development/core/public/kibana-plugin-public.httperrorrequest.error.md b/docs/development/core/public/kibana-plugin-public.httperrorrequest.error.md deleted file mode 100644 index d1b7417ecfa437..00000000000000 --- a/docs/development/core/public/kibana-plugin-public.httperrorrequest.error.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpErrorRequest](./kibana-plugin-public.httperrorrequest.md) > [error](./kibana-plugin-public.httperrorrequest.error.md) - -## HttpErrorRequest.error property - -Signature: - -```typescript -error: Error; -``` diff --git a/docs/development/core/public/kibana-plugin-public.httperrorrequest.md b/docs/development/core/public/kibana-plugin-public.httperrorrequest.md deleted file mode 100644 index 5f86bdb0afbd39..00000000000000 --- a/docs/development/core/public/kibana-plugin-public.httperrorrequest.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpErrorRequest](./kibana-plugin-public.httperrorrequest.md) - -## HttpErrorRequest interface - - -Signature: - -```typescript -export interface HttpErrorRequest -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [error](./kibana-plugin-public.httperrorrequest.error.md) | Error | | -| [request](./kibana-plugin-public.httperrorrequest.request.md) | Request | | - diff --git a/docs/development/core/public/kibana-plugin-public.httperrorrequest.request.md b/docs/development/core/public/kibana-plugin-public.httperrorrequest.request.md deleted file mode 100644 index 0a00ad627623be..00000000000000 --- a/docs/development/core/public/kibana-plugin-public.httperrorrequest.request.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpErrorRequest](./kibana-plugin-public.httperrorrequest.md) > [request](./kibana-plugin-public.httperrorrequest.request.md) - -## HttpErrorRequest.request property - -Signature: - -```typescript -request: Request; -``` diff --git a/docs/development/core/public/kibana-plugin-public.httperrorresponse.error.md b/docs/development/core/public/kibana-plugin-public.httperrorresponse.error.md deleted file mode 100644 index e9667286101ad0..00000000000000 --- a/docs/development/core/public/kibana-plugin-public.httperrorresponse.error.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpErrorResponse](./kibana-plugin-public.httperrorresponse.md) > [error](./kibana-plugin-public.httperrorresponse.error.md) - -## HttpErrorResponse.error property - -Signature: - -```typescript -error: Error | IHttpFetchError; -``` diff --git a/docs/development/core/public/kibana-plugin-public.httperrorresponse.md b/docs/development/core/public/kibana-plugin-public.httperrorresponse.md deleted file mode 100644 index 052d729a834921..00000000000000 --- a/docs/development/core/public/kibana-plugin-public.httperrorresponse.md +++ /dev/null @@ -1,19 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpErrorResponse](./kibana-plugin-public.httperrorresponse.md) - -## HttpErrorResponse interface - - -Signature: - -```typescript -export interface HttpErrorResponse extends IHttpResponse -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [error](./kibana-plugin-public.httperrorresponse.error.md) | Error | IHttpFetchError | | - diff --git a/docs/development/core/public/kibana-plugin-public.httpfetchoptions.asresponse.md b/docs/development/core/public/kibana-plugin-public.httpfetchoptions.asresponse.md index 5cb8e8ab0367ac..f1661cdb64b4a4 100644 --- a/docs/development/core/public/kibana-plugin-public.httpfetchoptions.asresponse.md +++ b/docs/development/core/public/kibana-plugin-public.httpfetchoptions.asresponse.md @@ -4,7 +4,7 @@ ## 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`. +When `true` the return type of [HttpHandler](./kibana-plugin-public.httphandler.md) will be an [HttpResponse](./kibana-plugin-public.httpresponse.md) with detailed request and response information. When `false`, the return type will just be the parsed response body. Defaults to `false`. Signature: diff --git a/docs/development/core/public/kibana-plugin-public.httpfetchoptions.assystemrequest.md b/docs/development/core/public/kibana-plugin-public.httpfetchoptions.assystemrequest.md new file mode 100644 index 00000000000000..609e4dd410601b --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpfetchoptions.assystemrequest.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) > [asSystemRequest](./kibana-plugin-public.httpfetchoptions.assystemrequest.md) + +## HttpFetchOptions.asSystemRequest property + +Whether or not the request should include the "system request" header to differentiate an end user request from Kibana internal request. Can be read on the server-side using KibanaRequest\#isSystemRequest. Defaults to `false`. + +Signature: + +```typescript +asSystemRequest?: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httpfetchoptions.md b/docs/development/core/public/kibana-plugin-public.httpfetchoptions.md index ab3431d86bb672..b7620f9e042dbc 100644 --- a/docs/development/core/public/kibana-plugin-public.httpfetchoptions.md +++ b/docs/development/core/public/kibana-plugin-public.httpfetchoptions.md @@ -16,7 +16,8 @@ export interface HttpFetchOptions extends HttpRequestInit | Property | Type | Description | | --- | --- | --- | -| [asResponse](./kibana-plugin-public.httpfetchoptions.asresponse.md) | boolean | 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. | +| [asResponse](./kibana-plugin-public.httpfetchoptions.asresponse.md) | boolean | When true the return type of [HttpHandler](./kibana-plugin-public.httphandler.md) will be an [HttpResponse](./kibana-plugin-public.httpresponse.md) with detailed request and response information. When false, the return type will just be the parsed response body. Defaults to false. | +| [asSystemRequest](./kibana-plugin-public.httpfetchoptions.assystemrequest.md) | boolean | Whether or not the request should include the "system request" header to differentiate an end user request from Kibana internal request. Can be read on the server-side using KibanaRequest\#isSystemRequest. Defaults to false. | | [headers](./kibana-plugin-public.httpfetchoptions.headers.md) | HttpHeadersInit | Headers to send with the request. See [HttpHeadersInit](./kibana-plugin-public.httpheadersinit.md). | | [prependBasePath](./kibana-plugin-public.httpfetchoptions.prependbasepath.md) | boolean | Whether or not the request should automatically prepend the basePath. Defaults to true. | | [query](./kibana-plugin-public.httpfetchoptions.query.md) | HttpFetchQuery | The query string for an HTTP request. See [HttpFetchQuery](./kibana-plugin-public.httpfetchquery.md). | diff --git a/docs/development/core/public/kibana-plugin-public.httpfetchoptionswithpath.md b/docs/development/core/public/kibana-plugin-public.httpfetchoptionswithpath.md new file mode 100644 index 00000000000000..5c27122e07ba79 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpfetchoptionswithpath.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpFetchOptionsWithPath](./kibana-plugin-public.httpfetchoptionswithpath.md) + +## HttpFetchOptionsWithPath interface + +Similar to [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) but with the URL path included. + +Signature: + +```typescript +export interface HttpFetchOptionsWithPath extends HttpFetchOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [path](./kibana-plugin-public.httpfetchoptionswithpath.path.md) | string | | + diff --git a/docs/development/core/public/kibana-plugin-public.httpfetchoptionswithpath.path.md b/docs/development/core/public/kibana-plugin-public.httpfetchoptionswithpath.path.md new file mode 100644 index 00000000000000..be84a6315564ee --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpfetchoptionswithpath.path.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpFetchOptionsWithPath](./kibana-plugin-public.httpfetchoptionswithpath.md) > [path](./kibana-plugin-public.httpfetchoptionswithpath.path.md) + +## HttpFetchOptionsWithPath.path property + +Signature: + +```typescript +path: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httphandler.md b/docs/development/core/public/kibana-plugin-public.httphandler.md index 903855c6e5fb87..09d98fe97557f8 100644 --- a/docs/development/core/public/kibana-plugin-public.httphandler.md +++ b/docs/development/core/public/kibana-plugin-public.httphandler.md @@ -4,7 +4,7 @@ ## HttpHandler interface -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. +A function for making an HTTP requests to Kibana's backend. See [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) for options and [HttpResponse](./kibana-plugin-public.httpresponse.md) for the response. Signature: diff --git a/docs/development/core/public/kibana-plugin-public.httpheadersinit.md b/docs/development/core/public/kibana-plugin-public.httpheadersinit.md index cef31bc30b4387..a0d5fec388f872 100644 --- a/docs/development/core/public/kibana-plugin-public.httpheadersinit.md +++ b/docs/development/core/public/kibana-plugin-public.httpheadersinit.md @@ -4,6 +4,7 @@ ## HttpHeadersInit interface +Headers to append to the request. Any headers that begin with `kbn-` are considered private to Core and will cause [HttpHandler](./kibana-plugin-public.httphandler.md) to throw an error. Signature: diff --git a/docs/development/core/public/kibana-plugin-public.httpinterceptor.md b/docs/development/core/public/kibana-plugin-public.httpinterceptor.md index 6924571c3c26ab..1cf782b1ba749c 100644 --- a/docs/development/core/public/kibana-plugin-public.httpinterceptor.md +++ b/docs/development/core/public/kibana-plugin-public.httpinterceptor.md @@ -16,7 +16,7 @@ export interface HttpInterceptor | Method | Description | | --- | --- | -| [request(request, controller)](./kibana-plugin-public.httpinterceptor.request.md) | Define an interceptor to be executed before a request is sent. | +| [request(fetchOptions, controller)](./kibana-plugin-public.httpinterceptor.request.md) | Define an interceptor to be executed before a request is sent. | | [requestError(httpErrorRequest, controller)](./kibana-plugin-public.httpinterceptor.requesterror.md) | Define an interceptor to be executed if a request interceptor throws an error or returns a rejected Promise. | | [response(httpResponse, controller)](./kibana-plugin-public.httpinterceptor.response.md) | Define an interceptor to be executed after a response is received. | | [responseError(httpErrorResponse, controller)](./kibana-plugin-public.httpinterceptor.responseerror.md) | Define an interceptor to be executed if a response interceptor throws an error or returns a rejected Promise. | diff --git a/docs/development/core/public/kibana-plugin-public.httpinterceptor.request.md b/docs/development/core/public/kibana-plugin-public.httpinterceptor.request.md index d0847d0ec3c7c3..8a6812f40e4cd2 100644 --- a/docs/development/core/public/kibana-plugin-public.httpinterceptor.request.md +++ b/docs/development/core/public/kibana-plugin-public.httpinterceptor.request.md @@ -9,17 +9,17 @@ Define an interceptor to be executed before a request is sent. Signature: ```typescript -request?(request: Request, controller: IHttpInterceptController): Promise | Request | void; +request?(fetchOptions: Readonly, controller: IHttpInterceptController): MaybePromise> | void; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| request | Request | | +| fetchOptions | Readonly<HttpFetchOptionsWithPath> | | | controller | IHttpInterceptController | | Returns: -`Promise | Request | void` +`MaybePromise> | void` diff --git a/docs/development/core/public/kibana-plugin-public.httpinterceptor.requesterror.md b/docs/development/core/public/kibana-plugin-public.httpinterceptor.requesterror.md index 56c477e0a6587c..7bb9202aa905ec 100644 --- a/docs/development/core/public/kibana-plugin-public.httpinterceptor.requesterror.md +++ b/docs/development/core/public/kibana-plugin-public.httpinterceptor.requesterror.md @@ -9,17 +9,17 @@ Define an interceptor to be executed if a request interceptor throws an error or Signature: ```typescript -requestError?(httpErrorRequest: HttpErrorRequest, controller: IHttpInterceptController): Promise | Request | void; +requestError?(httpErrorRequest: HttpInterceptorRequestError, controller: IHttpInterceptController): MaybePromise> | void; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| httpErrorRequest | HttpErrorRequest | | +| httpErrorRequest | HttpInterceptorRequestError | | | controller | IHttpInterceptController | | Returns: -`Promise | Request | void` +`MaybePromise> | void` diff --git a/docs/development/core/public/kibana-plugin-public.httpinterceptor.response.md b/docs/development/core/public/kibana-plugin-public.httpinterceptor.response.md index 23a8c5a2aa25ab..12a5b36090abc1 100644 --- a/docs/development/core/public/kibana-plugin-public.httpinterceptor.response.md +++ b/docs/development/core/public/kibana-plugin-public.httpinterceptor.response.md @@ -9,17 +9,17 @@ Define an interceptor to be executed after a response is received. Signature: ```typescript -response?(httpResponse: IHttpResponse, controller: IHttpInterceptController): Promise | IHttpResponseInterceptorOverrides | void; +response?(httpResponse: HttpResponse, controller: IHttpInterceptController): MaybePromise | void; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| httpResponse | IHttpResponse | | +| httpResponse | HttpResponse | | | controller | IHttpInterceptController | | Returns: -`Promise | IHttpResponseInterceptorOverrides | void` +`MaybePromise | void` diff --git a/docs/development/core/public/kibana-plugin-public.httpinterceptor.responseerror.md b/docs/development/core/public/kibana-plugin-public.httpinterceptor.responseerror.md index 02d8d25879260f..d3c2b6db128c15 100644 --- a/docs/development/core/public/kibana-plugin-public.httpinterceptor.responseerror.md +++ b/docs/development/core/public/kibana-plugin-public.httpinterceptor.responseerror.md @@ -9,17 +9,17 @@ Define an interceptor to be executed if a response interceptor throws an error o Signature: ```typescript -responseError?(httpErrorResponse: HttpErrorResponse, controller: IHttpInterceptController): Promise | IHttpResponseInterceptorOverrides | void; +responseError?(httpErrorResponse: HttpInterceptorResponseError, controller: IHttpInterceptController): MaybePromise | void; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| httpErrorResponse | HttpErrorResponse | | +| httpErrorResponse | HttpInterceptorResponseError | | | controller | IHttpInterceptController | | Returns: -`Promise | IHttpResponseInterceptorOverrides | void` +`MaybePromise | void` diff --git a/docs/development/core/public/kibana-plugin-public.httpinterceptorrequesterror.error.md b/docs/development/core/public/kibana-plugin-public.httpinterceptorrequesterror.error.md new file mode 100644 index 00000000000000..2eeafffb8d5566 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpinterceptorrequesterror.error.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpInterceptorRequestError](./kibana-plugin-public.httpinterceptorrequesterror.md) > [error](./kibana-plugin-public.httpinterceptorrequesterror.error.md) + +## HttpInterceptorRequestError.error property + +Signature: + +```typescript +error: Error; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httpinterceptorrequesterror.fetchoptions.md b/docs/development/core/public/kibana-plugin-public.httpinterceptorrequesterror.fetchoptions.md new file mode 100644 index 00000000000000..31a7f8ef44d9f5 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpinterceptorrequesterror.fetchoptions.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpInterceptorRequestError](./kibana-plugin-public.httpinterceptorrequesterror.md) > [fetchOptions](./kibana-plugin-public.httpinterceptorrequesterror.fetchoptions.md) + +## HttpInterceptorRequestError.fetchOptions property + +Signature: + +```typescript +fetchOptions: Readonly; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httpinterceptorrequesterror.md b/docs/development/core/public/kibana-plugin-public.httpinterceptorrequesterror.md new file mode 100644 index 00000000000000..4174523ed5fa6a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpinterceptorrequesterror.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpInterceptorRequestError](./kibana-plugin-public.httpinterceptorrequesterror.md) + +## HttpInterceptorRequestError interface + + +Signature: + +```typescript +export interface HttpInterceptorRequestError +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [error](./kibana-plugin-public.httpinterceptorrequesterror.error.md) | Error | | +| [fetchOptions](./kibana-plugin-public.httpinterceptorrequesterror.fetchoptions.md) | Readonly<HttpFetchOptionsWithPath> | | + diff --git a/docs/development/core/public/kibana-plugin-public.httpinterceptorresponseerror.error.md b/docs/development/core/public/kibana-plugin-public.httpinterceptorresponseerror.error.md new file mode 100644 index 00000000000000..c1367ccdd580e7 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpinterceptorresponseerror.error.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpInterceptorResponseError](./kibana-plugin-public.httpinterceptorresponseerror.md) > [error](./kibana-plugin-public.httpinterceptorresponseerror.error.md) + +## HttpInterceptorResponseError.error property + +Signature: + +```typescript +error: Error | IHttpFetchError; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httpinterceptorresponseerror.md b/docs/development/core/public/kibana-plugin-public.httpinterceptorresponseerror.md new file mode 100644 index 00000000000000..d306f9c6096bd0 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpinterceptorresponseerror.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpInterceptorResponseError](./kibana-plugin-public.httpinterceptorresponseerror.md) + +## HttpInterceptorResponseError interface + + +Signature: + +```typescript +export interface HttpInterceptorResponseError extends HttpResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [error](./kibana-plugin-public.httpinterceptorresponseerror.error.md) | Error | IHttpFetchError | | +| [request](./kibana-plugin-public.httpinterceptorresponseerror.request.md) | Readonly<Request> | | + diff --git a/docs/development/core/public/kibana-plugin-public.httpinterceptorresponseerror.request.md b/docs/development/core/public/kibana-plugin-public.httpinterceptorresponseerror.request.md new file mode 100644 index 00000000000000..d2f2826c042831 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpinterceptorresponseerror.request.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpInterceptorResponseError](./kibana-plugin-public.httpinterceptorresponseerror.md) > [request](./kibana-plugin-public.httpinterceptorresponseerror.request.md) + +## HttpInterceptorResponseError.request property + +Signature: + +```typescript +request: Readonly; +``` diff --git a/docs/development/core/public/kibana-plugin-public.ihttpresponse.body.md b/docs/development/core/public/kibana-plugin-public.httpresponse.body.md similarity index 63% rename from docs/development/core/public/kibana-plugin-public.ihttpresponse.body.md rename to docs/development/core/public/kibana-plugin-public.httpresponse.body.md index 9beec3a521ca3e..773812135602b7 100644 --- a/docs/development/core/public/kibana-plugin-public.ihttpresponse.body.md +++ b/docs/development/core/public/kibana-plugin-public.httpresponse.body.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) > [body](./kibana-plugin-public.ihttpresponse.body.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpResponse](./kibana-plugin-public.httpresponse.md) > [body](./kibana-plugin-public.httpresponse.body.md) -## IHttpResponse.body property +## HttpResponse.body property Parsed body received, may be undefined if there was an error. diff --git a/docs/development/core/public/kibana-plugin-public.httpresponse.fetchoptions.md b/docs/development/core/public/kibana-plugin-public.httpresponse.fetchoptions.md new file mode 100644 index 00000000000000..8fd4f8d1e908ed --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpresponse.fetchoptions.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpResponse](./kibana-plugin-public.httpresponse.md) > [fetchOptions](./kibana-plugin-public.httpresponse.fetchoptions.md) + +## HttpResponse.fetchOptions property + +The original [HttpFetchOptionsWithPath](./kibana-plugin-public.httpfetchoptionswithpath.md) used to send this request. + +Signature: + +```typescript +readonly fetchOptions: Readonly; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httpresponse.md b/docs/development/core/public/kibana-plugin-public.httpresponse.md new file mode 100644 index 00000000000000..3e70e5556b982e --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpresponse.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpResponse](./kibana-plugin-public.httpresponse.md) + +## HttpResponse interface + + +Signature: + +```typescript +export interface HttpResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [body](./kibana-plugin-public.httpresponse.body.md) | TResponseBody | Parsed body received, may be undefined if there was an error. | +| [fetchOptions](./kibana-plugin-public.httpresponse.fetchoptions.md) | Readonly<HttpFetchOptionsWithPath> | The original [HttpFetchOptionsWithPath](./kibana-plugin-public.httpfetchoptionswithpath.md) used to send this request. | +| [request](./kibana-plugin-public.httpresponse.request.md) | Readonly<Request> | Raw request sent to Kibana server. | +| [response](./kibana-plugin-public.httpresponse.response.md) | Readonly<Response> | Raw response received, may be undefined if there was an error. | + diff --git a/docs/development/core/public/kibana-plugin-public.ihttpresponse.request.md b/docs/development/core/public/kibana-plugin-public.httpresponse.request.md similarity index 60% rename from docs/development/core/public/kibana-plugin-public.ihttpresponse.request.md rename to docs/development/core/public/kibana-plugin-public.httpresponse.request.md index 743965137f2c44..583a295e26a72e 100644 --- a/docs/development/core/public/kibana-plugin-public.ihttpresponse.request.md +++ b/docs/development/core/public/kibana-plugin-public.httpresponse.request.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) > [request](./kibana-plugin-public.ihttpresponse.request.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpResponse](./kibana-plugin-public.httpresponse.md) > [request](./kibana-plugin-public.httpresponse.request.md) -## IHttpResponse.request property +## HttpResponse.request property Raw request sent to Kibana server. diff --git a/docs/development/core/public/kibana-plugin-public.ihttpresponse.response.md b/docs/development/core/public/kibana-plugin-public.httpresponse.response.md similarity index 62% rename from docs/development/core/public/kibana-plugin-public.ihttpresponse.response.md rename to docs/development/core/public/kibana-plugin-public.httpresponse.response.md index eed7d437fa4a2c..b773b3a4d5b557 100644 --- a/docs/development/core/public/kibana-plugin-public.ihttpresponse.response.md +++ b/docs/development/core/public/kibana-plugin-public.httpresponse.response.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) > [response](./kibana-plugin-public.ihttpresponse.response.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpResponse](./kibana-plugin-public.httpresponse.md) > [response](./kibana-plugin-public.httpresponse.response.md) -## IHttpResponse.response property +## HttpResponse.response property Raw response received, may be undefined if there was an error. diff --git a/docs/development/core/public/kibana-plugin-public.ihttpresponse.md b/docs/development/core/public/kibana-plugin-public.ihttpresponse.md deleted file mode 100644 index dd3b24880563bb..00000000000000 --- a/docs/development/core/public/kibana-plugin-public.ihttpresponse.md +++ /dev/null @@ -1,21 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) - -## IHttpResponse interface - - -Signature: - -```typescript -export interface IHttpResponse -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [body](./kibana-plugin-public.ihttpresponse.body.md) | TResponseBody | Parsed body received, may be undefined if there was an error. | -| [request](./kibana-plugin-public.ihttpresponse.request.md) | Readonly<Request> | Raw request sent to Kibana server. | -| [response](./kibana-plugin-public.ihttpresponse.response.md) | Readonly<Response> | Raw response received, may be undefined if there was an error. | - diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index 64f14b9b7dba45..95a4327728139f 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -59,14 +59,16 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ErrorToastOptions](./kibana-plugin-public.errortoastoptions.md) | Options available for [IToasts](./kibana-plugin-public.itoasts.md) APIs. | | [FatalErrorInfo](./kibana-plugin-public.fatalerrorinfo.md) | Represents the message and stack of a fatal Error | | [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. | -| [HttpErrorRequest](./kibana-plugin-public.httperrorrequest.md) | | -| [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). | +| [HttpFetchOptionsWithPath](./kibana-plugin-public.httpfetchoptionswithpath.md) | Similar to [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) but with the URL path included. | | [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) | | +| [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 [HttpResponse](./kibana-plugin-public.httpresponse.md) for the response. | +| [HttpHeadersInit](./kibana-plugin-public.httpheadersinit.md) | Headers to append to the request. Any headers that begin with kbn- are considered private to Core and will cause [HttpHandler](./kibana-plugin-public.httphandler.md) to throw an error. | | [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). | +| [HttpInterceptorRequestError](./kibana-plugin-public.httpinterceptorrequesterror.md) | | +| [HttpInterceptorResponseError](./kibana-plugin-public.httpinterceptorresponseerror.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) | | | [HttpSetup](./kibana-plugin-public.httpsetup.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 | @@ -74,7 +76,6 @@ 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). | -| [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) | | | [IHttpResponseInterceptorOverrides](./kibana-plugin-public.ihttpresponseinterceptoroverrides.md) | Properties that can be returned by HttpInterceptor.request to override the response. | | [ImageValidation](./kibana-plugin-public.imagevalidation.md) | | | [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) | diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.issystemrequest.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.issystemrequest.md new file mode 100644 index 00000000000000..a643c70632d539 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.issystemrequest.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [isSystemRequest](./kibana-plugin-server.kibanarequest.issystemrequest.md) + +## KibanaRequest.isSystemRequest property + +Whether or not the request is a "system request" rather than an application-level request. Can be set on the client using the `HttpFetchOptions#asSystemRequest` option. + +Signature: + +```typescript +readonly isSystemRequest: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.md index c8c04d80502cc9..cb6745623e3818 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequest.md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.md @@ -25,6 +25,7 @@ export declare class KibanaRequestBody | | | [events](./kibana-plugin-server.kibanarequest.events.md) | | KibanaRequestEvents | Request events [KibanaRequestEvents](./kibana-plugin-server.kibanarequestevents.md) | | [headers](./kibana-plugin-server.kibanarequest.headers.md) | | Headers | Readonly copy of incoming request headers. | +| [isSystemRequest](./kibana-plugin-server.kibanarequest.issystemrequest.md) | | boolean | Whether or not the request is a "system request" rather than an application-level request. Can be set on the client using the HttpFetchOptions#asSystemRequest option. | | [params](./kibana-plugin-server.kibanarequest.params.md) | | Params | | | [query](./kibana-plugin-server.kibanarequest.query.md) | | Query | | | [route](./kibana-plugin-server.kibanarequest.route.md) | | RecursiveReadonly<KibanaRequestRoute<Method>> | matched route details | diff --git a/packages/kbn-utility-types/index.ts b/packages/kbn-utility-types/index.ts index 36bbc8cc828735..83a41a52aca381 100644 --- a/packages/kbn-utility-types/index.ts +++ b/packages/kbn-utility-types/index.ts @@ -21,7 +21,17 @@ import { PromiseType } from 'utility-types'; export { $Values, Required, Optional, Class } from 'utility-types'; /** - * Returns wrapped type of a promise. + * A type that may or may not be a `Promise`. + */ +export type MaybePromise = T | Promise; + +/** + * Converts a type to a `Promise`, unless it is already a `Promise`. Useful when proxying the return value of a possibly async function. + */ +export type ShallowPromise = T extends Promise ? Promise : Promise; + +/** + * Returns wrapped type of a `Promise`. */ export type UnwrapPromise> = PromiseType; @@ -39,11 +49,6 @@ export type UnwrapObservable> = T extends Observab ? U : never; -/** - * Converts a type to a `Promise`, unless it is already a `Promise`. Useful when proxying the return value of a possibly async function. - */ -export type ShallowPromise = T extends Promise ? Promise : Promise; - /** * Ensures T is of type X. */ diff --git a/src/core/public/http/fetch.test.ts b/src/core/public/http/fetch.test.ts index adb3d696a962fd..a99b7607d71491 100644 --- a/src/core/public/http/fetch.test.ts +++ b/src/core/public/http/fetch.test.ts @@ -24,7 +24,7 @@ import { join } from 'path'; import { Fetch } from './fetch'; import { BasePath } from './base_path'; -import { IHttpResponse } from './types'; +import { HttpResponse, HttpFetchOptionsWithPath } from './types'; function delay(duration: number) { return new Promise(r => setTimeout(r, duration)); @@ -40,6 +40,19 @@ describe('Fetch', () => { }); describe('http requests', () => { + it('should fail with invalid arguments', async () => { + fetchMock.get('*', {}); + await expect( + fetchInstance.fetch( + // @ts-ignore + { path: '/', headers: { hello: 'world' } }, + { headers: { hello: 'mars' } } + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Invalid fetch arguments, must either be (string, object) or (object, undefined), received (object, object)"` + ); + }); + it('should use supplied request method', async () => { fetchMock.post('*', {}); await fetchInstance.fetch('/my/path', { method: 'POST' }); @@ -56,6 +69,15 @@ describe('Fetch', () => { }); }); + it('should not set Content-Type if undefined', async () => { + fetchMock.get('*', {}); + await fetchInstance.fetch('/my/path', { headers: { 'Content-Type': undefined } }); + + expect(fetchMock.lastOptions()!.headers).toMatchObject({ + 'kbn-version': 'VERSION', + }); + }); + it('should use supplied pathname and querystring', async () => { fetchMock.get('*', {}); await fetchInstance.fetch('/my/path', { query: { a: 'b' } }); @@ -69,13 +91,106 @@ describe('Fetch', () => { headers: { myHeader: 'foo' }, }); - expect(fetchMock.lastOptions()!.headers).toEqual({ + expect(fetchMock.lastOptions()!.headers).toMatchObject({ 'content-type': 'application/json', 'kbn-version': 'VERSION', myheader: 'foo', }); }); + it('should not allow overwriting of kbn-version header', async () => { + fetchMock.get('*', {}); + await expect( + fetchInstance.fetch('/my/path', { + headers: { myHeader: 'foo', 'kbn-version': 'CUSTOM!' }, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Invalid fetch headers, headers beginning with \\"kbn-\\" are not allowed: [kbn-version]"` + ); + }); + + it('should not set kbn-system-request header by default', async () => { + fetchMock.get('*', {}); + await fetchInstance.fetch('/my/path', { + headers: { myHeader: 'foo' }, + }); + + expect(fetchMock.lastOptions()!.headers['kbn-system-request']).toBeUndefined(); + }); + + it('should not set kbn-system-request header when asSystemRequest: false', async () => { + fetchMock.get('*', {}); + await fetchInstance.fetch('/my/path', { + headers: { myHeader: 'foo' }, + asSystemRequest: false, + }); + + expect(fetchMock.lastOptions()!.headers['kbn-system-request']).toBeUndefined(); + }); + + it('should set kbn-system-request header when asSystemRequest: true', async () => { + fetchMock.get('*', {}); + await fetchInstance.fetch('/my/path', { + headers: { myHeader: 'foo' }, + asSystemRequest: true, + }); + + expect(fetchMock.lastOptions()!.headers).toMatchObject({ + 'kbn-system-request': 'true', + myheader: 'foo', + }); + }); + + it('should not allow overwriting of kbn-system-request when asSystemRequest: true', async () => { + fetchMock.get('*', {}); + await expect( + fetchInstance.fetch('/my/path', { + headers: { myHeader: 'foo', 'kbn-system-request': 'ANOTHER!' }, + asSystemRequest: true, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Invalid fetch headers, headers beginning with \\"kbn-\\" are not allowed: [kbn-system-request]"` + ); + }); + + it('should not allow overwriting of kbn-system-request when asSystemRequest: false', async () => { + fetchMock.get('*', {}); + await expect( + fetchInstance.fetch('/my/path', { + headers: { myHeader: 'foo', 'kbn-system-request': 'ANOTHER!' }, + asSystemRequest: false, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Invalid fetch headers, headers beginning with \\"kbn-\\" are not allowed: [kbn-system-request]"` + ); + }); + + // Deprecated header used by legacy platform pre-7.7. Remove in 8.x. + it('should not allow overwriting of kbn-system-api when asSystemRequest: true', async () => { + fetchMock.get('*', {}); + await expect( + fetchInstance.fetch('/my/path', { + headers: { myHeader: 'foo', 'kbn-system-api': 'ANOTHER!' }, + asSystemRequest: true, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Invalid fetch headers, headers beginning with \\"kbn-\\" are not allowed: [kbn-system-api]"` + ); + }); + + // Deprecated header used by legacy platform pre-7.7. Remove in 8.x. + it('should not allow overwriting of kbn-system-api when asSystemRequest: false', async () => { + fetchMock.get('*', {}); + await expect( + fetchInstance.fetch('/my/path', { + headers: { myHeader: 'foo', 'kbn-system-api': 'ANOTHER!' }, + asSystemRequest: false, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Invalid fetch headers, headers beginning with \\"kbn-\\" are not allowed: [kbn-system-api]"` + ); + }); + it('should return response', async () => { fetchMock.get('*', { foo: 'bar' }); const json = await fetchInstance.fetch('/my/path'); @@ -121,11 +236,35 @@ describe('Fetch', () => { const response = await fetchInstance.fetch('/my/path', { asResponse: true }); + expect(response.fetchOptions).toMatchObject({ + path: '/my/path', + asResponse: true, + }); expect(response.request).toBeInstanceOf(Request); expect(response.response).toBeInstanceOf(Response); expect(response.body).toEqual({ foo: 'bar' }); }); + it('should expose asSystemRequest: true on detailed response object when asResponse = true', async () => { + fetchMock.get('*', { foo: 'bar' }); + + const response = await fetchInstance.fetch('/my/path', { + asResponse: true, + asSystemRequest: true, + }); + expect(response.fetchOptions.asSystemRequest).toBe(true); + }); + + it('should expose asSystemRequest: false on detailed response object when asResponse = true', async () => { + fetchMock.get('*', { foo: 'bar' }); + + const response = await fetchInstance.fetch('/my/path', { + asResponse: true, + asSystemRequest: false, + }); + expect(response.fetchOptions.asSystemRequest).toBe(false); + }); + it('should reject on network error', async () => { expect.assertions(1); fetchMock.get('*', { status: 500 }); @@ -245,13 +384,18 @@ describe('Fetch', () => { it('should be able to manipulate request instance', async () => { fetchInstance.intercept({ - request(request) { - request.headers.set('Content-Type', 'CustomContentType'); + request(options) { + return { + headers: { + ...options.headers, + 'Content-Type': 'CustomContentType', + }, + }; }, }); fetchInstance.intercept({ - request(request) { - return new Request('/my/route', request); + request() { + return { path: '/my/route' }; }, }); @@ -262,7 +406,7 @@ describe('Fetch', () => { expect(fetchMock.lastOptions()!.headers).toMatchObject({ 'content-type': 'CustomContentType', }); - expect(fetchMock.lastUrl()).toBe('/my/route'); + expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/route'); }); it('should call interceptors in correct order', async () => { @@ -402,8 +546,8 @@ describe('Fetch', () => { fetchInstance.intercept({ request: unusedSpy, - requestError({ request }) { - return new Request('/my/route', request); + requestError() { + return { path: '/my/route' }; }, response: usedSpy, }); @@ -423,16 +567,16 @@ describe('Fetch', () => { it('should accumulate request information', async () => { const routes = ['alpha', 'beta', 'gamma']; - const createRequest = jest.fn( - (request: Request) => new Request(`/api/${routes.shift()}`, request) - ); + const createRequest = jest.fn((options: HttpFetchOptionsWithPath) => ({ + path: `/api/${routes.shift()}`, + })); fetchInstance.intercept({ request: createRequest, }); fetchInstance.intercept({ requestError(httpErrorRequest) { - return httpErrorRequest.request; + return httpErrorRequest.fetchOptions; }, }); fetchInstance.intercept({ @@ -450,15 +594,15 @@ describe('Fetch', () => { await expect(fetchInstance.fetch('/my/route')).resolves.toEqual({ foo: 'bar' }); expect(fetchMock.called()).toBe(true); expect(routes.length).toBe(0); - expect(createRequest.mock.calls[0][0].url).toContain('/my/route'); - expect(createRequest.mock.calls[1][0].url).toContain('/api/alpha'); - expect(createRequest.mock.calls[2][0].url).toContain('/api/beta'); + expect(createRequest.mock.calls[0][0].path).toContain('/my/route'); + expect(createRequest.mock.calls[1][0].path).toContain('/api/alpha'); + expect(createRequest.mock.calls[2][0].path).toContain('/api/beta'); expect(fetchMock.lastCall()!.request.url).toContain('/api/gamma'); }); it('should accumulate response information', async () => { const bodies = ['alpha', 'beta', 'gamma']; - const createResponse = jest.fn((httpResponse: IHttpResponse) => ({ + const createResponse = jest.fn((httpResponse: HttpResponse) => ({ body: bodies.shift(), })); @@ -550,7 +694,7 @@ describe('Fetch', () => { fetchInstance.intercept({ requestError(httpErrorRequest) { - return httpErrorRequest.request; + return httpErrorRequest.fetchOptions; }, response: usedSpy, }); diff --git a/src/core/public/http/fetch.ts b/src/core/public/http/fetch.ts index b7ceaed6e56a76..1043b50dff9584 100644 --- a/src/core/public/http/fetch.ts +++ b/src/core/public/http/fetch.ts @@ -20,10 +20,16 @@ import { merge } from 'lodash'; import { format } from 'url'; -import { IBasePath, HttpInterceptor, HttpHandler, HttpFetchOptions, IHttpResponse } from './types'; +import { + IBasePath, + HttpInterceptor, + HttpHandler, + HttpFetchOptions, + HttpResponse, + HttpFetchOptionsWithPath, +} 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'; @@ -60,29 +66,30 @@ export class Fetch { public readonly put = this.shorthand('PUT'); public fetch: HttpHandler = async ( - path: string, - options: HttpFetchOptions = {} + pathOrOptions: string | HttpFetchOptionsWithPath, + options?: HttpFetchOptions ) => { - const initialRequest = this.createRequest(path, options); + const optionsWithPath = validateFetchArguments(pathOrOptions, 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>(async (resolve, reject) => { + return new Promise>(async (resolve, reject) => { try { - const interceptedRequest = await interceptRequest( - initialRequest, + const interceptedOptions = await interceptRequest( + optionsWithPath, this.interceptors, controller ); - const initialResponse = this.fetchResponse(interceptedRequest); + const initialResponse = this.fetchResponse(interceptedOptions); const interceptedResponse = await interceptResponse( + interceptedOptions, initialResponse, this.interceptors, controller ); - if (options.asResponse) { + if (optionsWithPath.asResponse) { resolve(interceptedResponse); } else { resolve(interceptedResponse.body); @@ -95,38 +102,44 @@ export class Fetch { }); }; - private createRequest(path: string, options?: HttpFetchOptions): Request { + private createRequest(options: HttpFetchOptionsWithPath): Request { // Merge and destructure options out that are not applicable to the Fetch API. - const { query, prependBasePath: shouldPrependBasePath, asResponse, ...fetchOptions } = merge( + const { + query, + prependBasePath: shouldPrependBasePath, + asResponse, + asSystemRequest, + ...fetchOptions + } = merge( { method: 'GET', credentials: 'same-origin', prependBasePath: true, + }, + options, + { headers: { - 'kbn-version': this.params.kibanaVersion, 'Content-Type': 'application/json', + ...options.headers, + 'kbn-version': this.params.kibanaVersion, }, - }, - options || {} + } ); const url = format({ - pathname: shouldPrependBasePath ? this.params.basePath.prepend(path) : path, + pathname: shouldPrependBasePath ? this.params.basePath.prepend(options.path) : options.path, query, }); - if ( - options && - options.headers && - 'Content-Type' in options.headers && - options.headers['Content-Type'] === undefined - ) { - delete fetchOptions.headers['Content-Type']; + // Make sure the system request header is only present if `asSystemRequest` is true. + if (asSystemRequest) { + fetchOptions.headers['kbn-system-request'] = 'true'; } return new Request(url, fetchOptions); } - private async fetchResponse(request: Request) { + private async fetchResponse(fetchOptions: HttpFetchOptionsWithPath): Promise> { + const request = this.createRequest(fetchOptions); let response: Response; let body = null; @@ -164,11 +177,46 @@ export class Fetch { throw new HttpFetchError(response.statusText, request, response, body); } - return new HttpResponse({ request, response, body }); + return { fetchOptions, request, response, body }; } - private shorthand(method: string) { - return (path: string, options: HttpFetchOptions = {}) => - this.fetch(path, { ...options, method }); + private shorthand(method: string): HttpHandler { + return (pathOrOptions: string | HttpFetchOptionsWithPath, options?: HttpFetchOptions) => { + const optionsWithPath = validateFetchArguments(pathOrOptions, options); + return this.fetch({ ...optionsWithPath, method }); + }; } } + +/** + * Ensure that the overloaded arguments to `HttpHandler` are valid. + */ +const validateFetchArguments = ( + pathOrOptions: string | HttpFetchOptionsWithPath, + options?: HttpFetchOptions +): HttpFetchOptionsWithPath => { + let fullOptions: HttpFetchOptionsWithPath; + + if (typeof pathOrOptions === 'string' && (typeof options === 'object' || options === undefined)) { + fullOptions = { ...options, path: pathOrOptions }; + } else if (typeof pathOrOptions === 'object' && options === undefined) { + fullOptions = pathOrOptions; + } else { + throw new Error( + `Invalid fetch arguments, must either be (string, object) or (object, undefined), received (${typeof pathOrOptions}, ${typeof options})` + ); + } + + const invalidHeaders = Object.keys(fullOptions.headers ?? {}).filter(headerName => + headerName.startsWith('kbn-') + ); + if (invalidHeaders.length) { + throw new Error( + `Invalid fetch headers, headers beginning with "kbn-" are not allowed: [${invalidHeaders.join( + ',' + )}]` + ); + } + + return fullOptions; +}; diff --git a/src/core/public/http/intercept.ts b/src/core/public/http/intercept.ts index e2a16565c61c43..bacc8748d26808 100644 --- a/src/core/public/http/intercept.ts +++ b/src/core/public/http/intercept.ts @@ -19,28 +19,31 @@ import { HttpInterceptController } from './http_intercept_controller'; import { HttpInterceptHaltError } from './http_intercept_halt_error'; -import { HttpInterceptor, IHttpResponse } from './types'; -import { HttpResponse } from './response'; +import { HttpInterceptor, HttpResponse, HttpFetchOptionsWithPath } from './types'; export async function interceptRequest( - request: Request, + options: HttpFetchOptionsWithPath, interceptors: ReadonlySet, controller: HttpInterceptController -): Promise { - let next = request; +): Promise { + let current: HttpFetchOptionsWithPath; return [...interceptors].reduceRight( (promise, interceptor) => promise.then( - async (current: Request) => { - next = current; + async fetchOptions => { + current = fetchOptions; checkHalt(controller); if (!interceptor.request) { - return current; + return fetchOptions; } - return (await interceptor.request(current, controller)) || current; + const overrides = await interceptor.request(current, controller); + return { + ...current, + ...overrides, + }; }, async error => { checkHalt(controller, error); @@ -49,26 +52,33 @@ export async function interceptRequest( throw error; } - const nextRequest = await interceptor.requestError({ error, request: next }, controller); + const overrides = await interceptor.requestError( + { error, fetchOptions: current }, + controller + ); - if (!nextRequest) { + if (!overrides) { throw error; } - next = nextRequest; - return next; + current = { + ...current, + ...overrides, + }; + return current; } ), - Promise.resolve(request) + Promise.resolve(options) ); } export async function interceptResponse( - responsePromise: Promise, + fetchOptions: HttpFetchOptionsWithPath, + responsePromise: Promise, interceptors: ReadonlySet, controller: HttpInterceptController -): Promise { - let current: IHttpResponse; +): Promise { + let current: HttpResponse; return await [...interceptors].reduce( (promise, interceptor) => @@ -83,10 +93,10 @@ export async function interceptResponse( const interceptorOverrides = (await interceptor.response(httpResponse, controller)) || {}; - return new HttpResponse({ + return { ...httpResponse, ...interceptorOverrides, - }); + }; }, async error => { const request = error.request || (current && current.request); @@ -101,6 +111,7 @@ export async function interceptResponse( const next = await interceptor.responseError( { error, + fetchOptions, request, response: error.response || (current && current.response), body: error.body || (current && current.body), @@ -114,7 +125,7 @@ export async function interceptResponse( throw error; } - return new HttpResponse({ ...next, request }); + return { ...next, request, fetchOptions }; } catch (err) { checkHalt(controller, err); throw err; diff --git a/src/core/public/http/response.ts b/src/core/public/http/response.ts deleted file mode 100644 index 706e7caaca9768..00000000000000 --- a/src/core/public/http/response.ts +++ /dev/null @@ -1,40 +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. - */ - -import { IHttpResponse } from './types'; - -export class HttpResponse implements IHttpResponse { - 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; - } -} diff --git a/src/core/public/http/types.ts b/src/core/public/http/types.ts index 27ffddc79cf653..c38b9da4429438 100644 --- a/src/core/public/http/types.ts +++ b/src/core/public/http/types.ts @@ -18,6 +18,7 @@ */ import { Observable } from 'rxjs'; +import { MaybePromise } from '@kbn/utility-types'; /** @public */ export interface HttpSetup { @@ -110,7 +111,11 @@ export interface IAnonymousPaths { register(path: string): void; } -/** @public */ +/** + * Headers to append to the request. Any headers that begin with `kbn-` are considered private to Core and will cause + * {@link HttpHandler} to throw an error. + * @public + */ export interface HttpHeadersInit { [name: string]: any; } @@ -217,30 +222,54 @@ export interface HttpFetchOptions extends HttpRequestInit { headers?: HttpHeadersInit; /** - * When `true` the return type of {@link HttpHandler} will be an {@link IHttpResponse} with detailed request and + * Whether or not the request should include the "system request" header to differentiate an end user request from + * Kibana internal request. + * Can be read on the server-side using KibanaRequest#isSystemRequest. Defaults to `false`. + */ + asSystemRequest?: boolean; + + /** + * When `true` the return type of {@link HttpHandler} will be an {@link HttpResponse} with detailed request and * response information. When `false`, the return type will just be the parsed response body. Defaults to `false`. */ asResponse?: boolean; } +/** + * Similar to {@link HttpFetchOptions} but with the URL path included. + * @public + */ +export interface HttpFetchOptionsWithPath extends HttpFetchOptions { + /* + * The path on the Kibana server to send the request to. Should not include the basePath. + */ + path: string; +} + /** * A function for making an HTTP requests to Kibana's backend. See {@link HttpFetchOptions} for options and - * {@link IHttpResponse} for the response. + * {@link HttpResponse} 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 IHttpResponse} + * @returns a Promise that resolves to a {@link HttpResponse} * @public */ export interface HttpHandler { (path: string, options: HttpFetchOptions & { asResponse: true }): Promise< - IHttpResponse + HttpResponse + >; + (options: HttpFetchOptionsWithPath & { asResponse: true }): Promise< + HttpResponse >; (path: string, options?: HttpFetchOptions): Promise; + (options: HttpFetchOptionsWithPath): Promise; } /** @public */ -export interface IHttpResponse { +export interface HttpResponse { + /** The original {@link HttpFetchOptionsWithPath} used to send this request. */ + readonly fetchOptions: Readonly; /** Raw request sent to Kibana server. */ readonly request: Readonly; /** Raw response received, may be undefined if there was an error. */ @@ -276,12 +305,13 @@ export interface IHttpFetchError extends Error { } /** @public */ -export interface HttpErrorResponse extends IHttpResponse { +export interface HttpInterceptorResponseError extends HttpResponse { + request: Readonly; error: Error | IHttpFetchError; } /** @public */ -export interface HttpErrorRequest { - request: Request; +export interface HttpInterceptorRequestError { + fetchOptions: Readonly; error: Error; } @@ -298,39 +328,39 @@ export interface HttpInterceptor { * @param controller {@link IHttpInterceptController} */ request?( - request: Request, + fetchOptions: Readonly, controller: IHttpInterceptController - ): Promise | Request | void; + ): MaybePromise> | void; /** * Define an interceptor to be executed if a request interceptor throws an error or returns a rejected Promise. - * @param httpErrorRequest {@link HttpErrorRequest} + * @param httpErrorRequest {@link HttpInterceptorRequestError} * @param controller {@link IHttpInterceptController} */ requestError?( - httpErrorRequest: HttpErrorRequest, + httpErrorRequest: HttpInterceptorRequestError, controller: IHttpInterceptController - ): Promise | Request | void; + ): MaybePromise> | void; /** * Define an interceptor to be executed after a response is received. - * @param httpResponse {@link IHttpResponse} + * @param httpResponse {@link HttpResponse} * @param controller {@link IHttpInterceptController} */ response?( - httpResponse: IHttpResponse, + httpResponse: HttpResponse, controller: IHttpInterceptController - ): Promise | IHttpResponseInterceptorOverrides | void; + ): MaybePromise | void; /** * Define an interceptor to be executed if a response interceptor throws an error or returns a rejected Promise. - * @param httpErrorResponse {@link HttpErrorResponse} + * @param httpErrorResponse {@link HttpInterceptorResponseError} * @param controller {@link IHttpInterceptController} */ responseError?( - httpErrorResponse: HttpErrorResponse, + httpErrorResponse: HttpInterceptorResponseError, controller: IHttpInterceptController - ): Promise | IHttpResponseInterceptorOverrides | void; + ): MaybePromise | void; } /** diff --git a/src/core/public/index.ts b/src/core/public/index.ts index a2a4a5c3b297ee..1bc660189384af 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -144,11 +144,12 @@ export { HttpHeadersInit, HttpRequestInit, HttpFetchOptions, + HttpFetchOptionsWithPath, HttpFetchQuery, - HttpErrorResponse, - HttpErrorRequest, + HttpInterceptorResponseError, + HttpInterceptorRequestError, HttpInterceptor, - IHttpResponse, + HttpResponse, HttpHandler, IBasePath, IAnonymousPaths, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index a9cea7ae97998d..5e36638516e569 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -9,6 +9,7 @@ import { EuiButtonEmptyProps } from '@elastic/eui'; import { EuiGlobalToastListToast } from '@elastic/eui'; import { ExclusiveUnion } from '@elastic/eui'; import { IconType } from '@elastic/eui'; +import { MaybePromise } from '@kbn/utility-types'; import { Observable } from 'rxjs'; import React from 'react'; import * as Rx from 'rxjs'; @@ -577,28 +578,21 @@ export type HandlerFunction = (context: T, ...args: any[]) => // @public export type HandlerParameters> = T extends (context: any, ...args: infer U) => any ? U : never; -// @public (undocumented) -export interface HttpErrorRequest { - // (undocumented) - error: Error; - // (undocumented) - request: Request; -} - -// @public (undocumented) -export interface HttpErrorResponse extends IHttpResponse { - // (undocumented) - error: Error | IHttpFetchError; -} - // @public export interface HttpFetchOptions extends HttpRequestInit { asResponse?: boolean; + asSystemRequest?: boolean; headers?: HttpHeadersInit; prependBasePath?: boolean; query?: HttpFetchQuery; } +// @public +export interface HttpFetchOptionsWithPath extends HttpFetchOptions { + // (undocumented) + path: string; +} + // @public (undocumented) export interface HttpFetchQuery { // (undocumented) @@ -610,12 +604,18 @@ export interface HttpHandler { // (undocumented) (path: string, options: HttpFetchOptions & { asResponse: true; - }): Promise>; + }): Promise>; + // (undocumented) + (options: HttpFetchOptionsWithPath & { + asResponse: true; + }): Promise>; // (undocumented) (path: string, options?: HttpFetchOptions): Promise; + // (undocumented) + (options: HttpFetchOptionsWithPath): Promise; } -// @public (undocumented) +// @public export interface HttpHeadersInit { // (undocumented) [name: string]: any; @@ -623,10 +623,26 @@ export interface HttpHeadersInit { // @public export interface HttpInterceptor { - request?(request: Request, controller: IHttpInterceptController): Promise | Request | void; - requestError?(httpErrorRequest: HttpErrorRequest, controller: IHttpInterceptController): Promise | Request | void; - response?(httpResponse: IHttpResponse, controller: IHttpInterceptController): Promise | IHttpResponseInterceptorOverrides | void; - responseError?(httpErrorResponse: HttpErrorResponse, controller: IHttpInterceptController): Promise | IHttpResponseInterceptorOverrides | void; + request?(fetchOptions: Readonly, controller: IHttpInterceptController): MaybePromise> | void; + requestError?(httpErrorRequest: HttpInterceptorRequestError, controller: IHttpInterceptController): MaybePromise> | void; + response?(httpResponse: HttpResponse, controller: IHttpInterceptController): MaybePromise | void; + responseError?(httpErrorResponse: HttpInterceptorResponseError, controller: IHttpInterceptController): MaybePromise | void; +} + +// @public (undocumented) +export interface HttpInterceptorRequestError { + // (undocumented) + error: Error; + // (undocumented) + fetchOptions: Readonly; +} + +// @public (undocumented) +export interface HttpInterceptorResponseError extends HttpResponse { + // (undocumented) + error: Error | IHttpFetchError; + // (undocumented) + request: Readonly; } // @public @@ -647,6 +663,14 @@ export interface HttpRequestInit { window?: null; } +// @public (undocumented) +export interface HttpResponse { + readonly body?: TResponseBody; + readonly fetchOptions: Readonly; + readonly request: Readonly; + readonly response?: Readonly; +} + // @public (undocumented) export interface HttpSetup { addLoadingCountSource(countSource$: Observable): void; @@ -718,13 +742,6 @@ export interface IHttpInterceptController { halted: boolean; } -// @public (undocumented) -export interface IHttpResponse { - readonly body?: TResponseBody; - readonly request: Readonly; - readonly response?: Readonly; -} - // @public export interface IHttpResponseInterceptorOverrides { readonly body?: TResponseBody; diff --git a/src/core/server/http/router/request.test.ts b/src/core/server/http/router/request.test.ts index 51162a2c258e91..032027c2344858 100644 --- a/src/core/server/http/router/request.test.ts +++ b/src/core/server/http/router/request.test.ts @@ -66,6 +66,57 @@ describe('KibanaRequest', () => { }); }); + describe('isSytemApi property', () => { + it('is false when no kbn-system-request header is set', () => { + const request = httpServerMock.createRawRequest({ + headers: { custom: 'one' }, + }); + const kibanaRequest = KibanaRequest.from(request); + expect(kibanaRequest.isSystemRequest).toBe(false); + }); + + it('is true when kbn-system-request header is set to true', () => { + const request = httpServerMock.createRawRequest({ + headers: { custom: 'one', 'kbn-system-request': 'true' }, + }); + const kibanaRequest = KibanaRequest.from(request); + expect(kibanaRequest.isSystemRequest).toBe(true); + }); + + it('is false when kbn-system-request header is set to false', () => { + const request = httpServerMock.createRawRequest({ + headers: { custom: 'one', 'kbn-system-request': 'false' }, + }); + const kibanaRequest = KibanaRequest.from(request); + expect(kibanaRequest.isSystemRequest).toBe(false); + }); + + // Remove support for kbn-system-api header in 8.x. Only used by legacy platform. + it('is false when no kbn-system-api header is set', () => { + const request = httpServerMock.createRawRequest({ + headers: { custom: 'one' }, + }); + const kibanaRequest = KibanaRequest.from(request); + expect(kibanaRequest.isSystemRequest).toBe(false); + }); + + it('is true when kbn-system-api header is set to true', () => { + const request = httpServerMock.createRawRequest({ + headers: { custom: 'one', 'kbn-system-api': 'true' }, + }); + const kibanaRequest = KibanaRequest.from(request); + expect(kibanaRequest.isSystemRequest).toBe(true); + }); + + it('is false when kbn-system-api header is set to false', () => { + const request = httpServerMock.createRawRequest({ + headers: { custom: 'one', 'kbn-system-api': 'false' }, + }); + const kibanaRequest = KibanaRequest.from(request); + expect(kibanaRequest.isSystemRequest).toBe(false); + }); + }); + describe('RouteSchema type inferring', () => { it('should work with config-schema', () => { const body = Buffer.from('body!'); diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index 22fb2d2643d1c7..703571ba53c0a3 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -127,6 +127,11 @@ export class KibanaRequest< * This property will contain a `filtered` copy of request headers. */ public readonly headers: Headers; + /** + * Whether or not the request is a "system request" rather than an application-level request. + * Can be set on the client using the `HttpFetchOptions#asSystemRequest` option. + */ + public readonly isSystemRequest: boolean; /** {@link IKibanaSocket} */ public readonly socket: IKibanaSocket; @@ -147,6 +152,10 @@ export class KibanaRequest< ) { this.url = request.url; this.headers = deepFreeze({ ...request.headers }); + this.isSystemRequest = + request.headers['kbn-system-request'] === 'true' || + // Remove support for `kbn-system-api` in 8.x. Used only by legacy platform. + request.headers['kbn-system-api'] === 'true'; // prevent Symbol exposure via Object.getOwnPropertySymbols() Object.defineProperty(this, requestSymbol, { diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 15d453db294ac3..e4ea06769007a1 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -888,6 +888,7 @@ export class KibanaRequest(req: Request, routeSchemas?: RouteValidator | RouteValidatorFullConfig, withoutSecretHeaders?: boolean): KibanaRequest; readonly headers: Headers; + readonly isSystemRequest: boolean; // (undocumented) readonly params: Params; // (undocumented) diff --git a/src/legacy/core_plugins/kibana/server/lib/system_api.js b/src/legacy/core_plugins/kibana/server/lib/system_api.js index 24c901a3db0a48..3e2ab667dd98bf 100644 --- a/src/legacy/core_plugins/kibana/server/lib/system_api.js +++ b/src/legacy/core_plugins/kibana/server/lib/system_api.js @@ -24,6 +24,7 @@ const SYSTEM_API_HEADER_NAME = 'kbn-system-api'; * * @param request HAPI request object * @return true if request is a system API request; false, otherwise + * @deprecated Use KibanaRequest#isSystemApi */ export function isSystemApiRequest(request) { return !!request.headers[SYSTEM_API_HEADER_NAME]; diff --git a/src/legacy/ui/public/kfetch/kfetch.ts b/src/legacy/ui/public/kfetch/kfetch.ts index cb96e03eb13283..02be7a32db2965 100644 --- a/src/legacy/ui/public/kfetch/kfetch.ts +++ b/src/legacy/ui/public/kfetch/kfetch.ts @@ -32,6 +32,7 @@ export interface KFetchQuery { export interface KFetchOptions extends HttpRequestInit { pathname: string; query?: KFetchQuery; + asSystemRequest?: boolean; } export interface KFetchKibanaOptions { diff --git a/src/plugins/data/public/search/sync_search_strategy.test.ts b/src/plugins/data/public/search/sync_search_strategy.test.ts index 2737a4033a015b..cd19c4d84dce14 100644 --- a/src/plugins/data/public/search/sync_search_strategy.test.ts +++ b/src/plugins/data/public/search/sync_search_strategy.test.ts @@ -44,10 +44,8 @@ describe('Sync search strategy', () => { }, {} ); - expect(mockCoreSetup.http.fetch.mock.calls[0][0]).toBe( - `/internal/search/${SYNC_SEARCH_STRATEGY}` - ); - expect(mockCoreSetup.http.fetch.mock.calls[0][1]).toEqual({ + expect(mockCoreSetup.http.fetch.mock.calls[0][0]).toEqual({ + path: `/internal/search/${SYNC_SEARCH_STRATEGY}`, body: JSON.stringify({ serverStrategy: 'SYNC_SEARCH_STRATEGY', }), diff --git a/src/plugins/data/public/search/sync_search_strategy.ts b/src/plugins/data/public/search/sync_search_strategy.ts index 3885a97a98571a..65fe10f39aaa04 100644 --- a/src/plugins/data/public/search/sync_search_strategy.ts +++ b/src/plugins/data/public/search/sync_search_strategy.ts @@ -36,14 +36,12 @@ export const syncSearchStrategyProvider: TSearchStrategyProvider { - const response: Promise = context.core.http.fetch( - `/internal/search/${request.serverStrategy}`, - { - method: 'POST', - body: JSON.stringify(request), - signal: options.signal, - } - ); + const response: Promise = context.core.http.fetch({ + path: `/internal/search/${request.serverStrategy}`, + method: 'POST', + body: JSON.stringify(request), + signal: options.signal, + }); return from(response); }; diff --git a/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx b/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx index bda1557bdaf915..39290b066fab7c 100644 --- a/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx +++ b/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx @@ -17,7 +17,7 @@ * under the License. */ -import { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/public'; +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'kibana/public'; import { CorePluginAPluginSetup } from '../../core_plugin_a/public/plugin'; declare global { @@ -52,7 +52,17 @@ export class CorePluginBPlugin }; } - public start() {} + public async start(core: CoreStart, deps: {}) { + return { + sendSystemRequest: async (asSystemRequest: boolean) => { + const response = await core.http.post('/core_plugin_b/system_request', { + asSystemRequest, + }); + return `/core_plugin_b/system_request says: "${response}"`; + }, + }; + } + public stop() {} } diff --git a/test/plugin_functional/plugins/core_plugin_b/server/plugin.ts b/test/plugin_functional/plugins/core_plugin_b/server/plugin.ts index 91a4fe89c1c57e..d2bb9222a23fa1 100644 --- a/test/plugin_functional/plugins/core_plugin_b/server/plugin.ts +++ b/test/plugin_functional/plugins/core_plugin_b/server/plugin.ts @@ -54,6 +54,16 @@ export class CorePluginBPlugin implements Plugin { return res.ok({ body: `ID: ${req.query.id} - ${req.body.bar.toUpperCase()}` }); } ); + + router.post( + { + path: '/core_plugin_b/system_request', + validate: false, + }, + async (context, req, res) => { + return res.ok({ body: `System request? ${req.isSystemRequest}` }); + } + ); } public start() {} diff --git a/test/plugin_functional/test_suites/core_plugins/server_plugins.ts b/test/plugin_functional/test_suites/core_plugins/server_plugins.ts index eb232b14589917..f5b45d36944e1d 100644 --- a/test/plugin_functional/test_suites/core_plugins/server_plugins.ts +++ b/test/plugin_functional/test_suites/core_plugins/server_plugins.ts @@ -54,5 +54,15 @@ export default function({ getService }: PluginFunctionalProviderContext) { statusCode: 400, }); }); + + it('sets request.isSystemRequest when kbn-system-request header is set', async () => { + await supertest + .post('/core_plugin_b/system_request') + .set('kbn-xsrf', 'anything') + .set('kbn-system-request', 'true') + .send() + .expect(200) + .expect('System request? true'); + }); }); } diff --git a/test/plugin_functional/test_suites/core_plugins/ui_plugins.ts b/test/plugin_functional/test_suites/core_plugins/ui_plugins.ts index b76463ee767393..82267d73782af0 100644 --- a/test/plugin_functional/test_suites/core_plugins/ui_plugins.ts +++ b/test/plugin_functional/test_suites/core_plugins/ui_plugins.ts @@ -64,7 +64,7 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider }); }); - describe('have env data provided', function describeIndexTests() { + describe('have env data provided', () => { before(async () => { await PageObjects.common.navigateToApp('bar'); }); @@ -75,5 +75,27 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider expect(envData.packageInfo.version).to.be.a('string'); }); }); + + describe('http fetching', () => { + before(async () => { + await PageObjects.common.navigateToApp('settings'); + }); + + it('should send kbn-system-request header when asSystemRequest: true', async () => { + expect( + await browser.executeAsync(async cb => { + window.__coreProvider.start.plugins.core_plugin_b.sendSystemRequest(true).then(cb); + }) + ).to.be('/core_plugin_b/system_request says: "System request? true"'); + }); + + it('should not send kbn-system-request header when asSystemRequest: false', async () => { + expect( + await browser.executeAsync(async cb => { + window.__coreProvider.start.plugins.core_plugin_b.sendSystemRequest(false).then(cb); + }) + ).to.be('/core_plugin_b/system_request says: "System request? false"'); + }); + }); }); } diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx index b04bd3a4e9be9d..e8a23b8e053c29 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx @@ -278,8 +278,8 @@ describe('IndexPattern Data Panel', () => { function testProps() { const setState = jest.fn(); - core.http.get.mockImplementation(async (url: string) => { - const parts = url.split('/'); + core.http.get.mockImplementation(async ({ path }) => { + const parts = path.split('/'); const indexPatternTitle = parts[parts.length - 1]; return { indexPatternTitle: `${indexPatternTitle}_testtitle`, @@ -397,7 +397,8 @@ describe('IndexPattern Data Panel', () => { expect(setState).toHaveBeenCalledTimes(2); expect(core.http.get).toHaveBeenCalledTimes(2); - expect(core.http.get).toHaveBeenCalledWith('/api/lens/existing_fields/a', { + expect(core.http.get).toHaveBeenCalledWith({ + path: '/api/lens/existing_fields/a', query: { fromDate: '2019-01-01', toDate: '2020-01-01', @@ -405,7 +406,8 @@ describe('IndexPattern Data Panel', () => { }, }); - expect(core.http.get).toHaveBeenCalledWith('/api/lens/existing_fields/a', { + expect(core.http.get).toHaveBeenCalledWith({ + path: '/api/lens/existing_fields/a', query: { fromDate: '2019-01-01', toDate: '2020-01-02', @@ -436,7 +438,8 @@ describe('IndexPattern Data Panel', () => { expect(setState).toHaveBeenCalledTimes(2); - expect(core.http.get).toHaveBeenCalledWith('/api/lens/existing_fields/a', { + expect(core.http.get).toHaveBeenCalledWith({ + path: '/api/lens/existing_fields/a', query: { fromDate: '2019-01-01', toDate: '2020-01-01', @@ -444,7 +447,8 @@ describe('IndexPattern Data Panel', () => { }, }); - expect(core.http.get).toHaveBeenCalledWith('/api/lens/existing_fields/b', { + expect(core.http.get).toHaveBeenCalledWith({ + path: '/api/lens/existing_fields/b', query: { fromDate: '2019-01-01', toDate: '2020-01-01', @@ -484,13 +488,13 @@ describe('IndexPattern Data Panel', () => { let overlapCount = 0; const props = testProps(); - core.http.get.mockImplementation((url: string) => { + core.http.get.mockImplementation(({ path }) => { if (queryCount) { ++overlapCount; } ++queryCount; - const parts = url.split('/'); + const parts = path.split('/'); const indexPatternTitle = parts[parts.length - 1]; const result = Promise.resolve({ indexPatternTitle, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.test.ts index 6bea13c32830fb..f8961c30d14ee2 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.test.ts @@ -537,8 +537,8 @@ describe('loader', () => { describe('syncExistingFields', () => { it('should call once for each index pattern', async () => { const setState = jest.fn(); - const fetchJson = jest.fn(async (url: string) => { - const indexPatternTitle = _.last(url.split('/')); + const fetchJson = jest.fn(({ path }: { path: string }) => { + const indexPatternTitle = _.last(path.split('/')); return { indexPatternTitle, existingFieldNames: ['field_1', 'field_2'].map( diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.ts index b9e166da3b01a4..c9473c18698685 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.ts @@ -235,7 +235,8 @@ export async function syncExistingFields({ query.timeFieldName = pattern.timeFieldName; } - return fetchJson(`${BASE_API_URL}/existing_fields/${pattern.id}`, { + return fetchJson({ + path: `${BASE_API_URL}/existing_fields/${pattern.id}`, query, }) as Promise; }) diff --git a/x-pack/legacy/plugins/reporting/public/lib/job_queue_client.ts b/x-pack/legacy/plugins/reporting/public/lib/job_queue_client.ts index 173a4e31cfef60..0f68c56a18bf69 100644 --- a/x-pack/legacy/plugins/reporting/public/lib/job_queue_client.ts +++ b/x-pack/legacy/plugins/reporting/public/lib/job_queue_client.ts @@ -5,8 +5,6 @@ */ import { kfetch } from 'ui/kfetch'; -// @ts-ignore -import { addSystemApiHeader } from 'ui/system_api'; const API_BASE_URL = '/api/reporting/jobs'; @@ -64,7 +62,7 @@ class JobQueueClient { method: 'GET', pathname: `${API_BASE_URL}/list`, query, - headers: addSystemApiHeader({}), + asSystemRequest: true, }); }; @@ -72,7 +70,7 @@ class JobQueueClient { return kfetch({ method: 'GET', pathname: `${API_BASE_URL}/count`, - headers: addSystemApiHeader({}), + asSystemRequest: true, }); } @@ -80,7 +78,7 @@ class JobQueueClient { return kfetch({ method: 'GET', pathname: `${API_BASE_URL}/output/${jobId}`, - headers: addSystemApiHeader({}), + asSystemRequest: true, }); } @@ -88,7 +86,7 @@ class JobQueueClient { return kfetch({ method: 'GET', pathname: `${API_BASE_URL}/info/${jobId}`, - headers: addSystemApiHeader({}), + asSystemRequest: true, }); } } diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js index 55da11221bb101..ae929045cf5709 100644 --- a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js +++ b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js @@ -16,6 +16,7 @@ const buildRequest = (path = '/app/kibana') => { return { path, route: { settings: {} }, + headers: {}, raw: { req: { socket: {}, diff --git a/x-pack/plugins/licensing/public/plugin.test.ts b/x-pack/plugins/licensing/public/plugin.test.ts index 01545ee8d48b4a..f68e1dcfaf62ba 100644 --- a/x-pack/plugins/licensing/public/plugin.test.ts +++ b/x-pack/plugins/licensing/public/plugin.test.ts @@ -64,10 +64,8 @@ describe('licensing plugin', () => { await refresh(); - expect(coreSetup.http.get.mock.calls[0][1]).toMatchObject({ - headers: { - 'kbn-system-api': 'true', - }, + expect(coreSetup.http.get.mock.calls[0][0]).toMatchObject({ + asSystemRequest: true, }); }); }); diff --git a/x-pack/plugins/licensing/public/plugin.ts b/x-pack/plugins/licensing/public/plugin.ts index 7d2498b0f7ff6e..dab4c4048ce4c4 100644 --- a/x-pack/plugins/licensing/public/plugin.ts +++ b/x-pack/plugins/licensing/public/plugin.ts @@ -132,10 +132,9 @@ export class LicensingPlugin implements Plugin { private fetchLicense = async (core: CoreSetup): Promise => { try { - const response = await core.http.get(this.infoEndpoint, { - headers: { - 'kbn-system-api': 'true', - }, + const response = await core.http.get({ + path: this.infoEndpoint, + asSystemRequest: true, }); return new License({ license: response.license, diff --git a/x-pack/plugins/security/public/authentication/authentication_service.ts b/x-pack/plugins/security/public/authentication/authentication_service.ts index 23c45c88e563a1..2679bc20d6a7d9 100644 --- a/x-pack/plugins/security/public/authentication/authentication_service.ts +++ b/x-pack/plugins/security/public/authentication/authentication_service.ts @@ -23,7 +23,7 @@ export class AuthenticationService { return { async getCurrentUser() { return (await http.get('/internal/security/me', { - headers: { 'kbn-system-api': true }, + asSystemRequest: true, })) as AuthenticatedUser; }, }; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx index 2b3d7c811f6de3..e183eae08d1e17 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx @@ -155,7 +155,7 @@ function getProps({ const { fatalErrors } = coreMock.createSetup(); const { http, docLinks, notifications } = coreMock.createStart(); - http.get.mockImplementation(async path => { + http.get.mockImplementation(async (path: any) => { if (path === '/api/features') { return buildFeatures(); } @@ -509,7 +509,7 @@ describe('', () => { it('can render if features are not available', async () => { const { http } = coreMock.createStart(); - http.get.mockImplementation(async path => { + http.get.mockImplementation(async (path: any) => { if (path === '/api/features') { const error = { response: { status: 404 } }; throw error; diff --git a/x-pack/plugins/security/public/management/roles/roles_api_client.test.ts b/x-pack/plugins/security/public/management/roles/roles_api_client.test.ts index 75611613684051..21a4f5c9499ec1 100644 --- a/x-pack/plugins/security/public/management/roles/roles_api_client.test.ts +++ b/x-pack/plugins/security/public/management/roles/roles_api_client.test.ts @@ -16,7 +16,7 @@ describe('RolesAPIClient', () => { await rolesAPIClient.saveRole({ role, spacesEnabled }); expect(httpMock.put).toHaveBeenCalledTimes(1); - return JSON.parse(httpMock.put.mock.calls[0][1]?.body as any); + return JSON.parse((httpMock.put.mock.calls[0] as any)[1]?.body as any); } describe('spaces disabled', () => { diff --git a/x-pack/plugins/security/public/session/session_timeout.tsx b/x-pack/plugins/security/public/session/session_timeout.tsx index e237201699b9be..bd6dbad7dbf149 100644 --- a/x-pack/plugins/security/public/session/session_timeout.tsx +++ b/x-pack/plugins/security/public/session/session_timeout.tsx @@ -104,9 +104,8 @@ export class SessionTimeout implements ISessionTimeout { */ private fetchSessionInfoAndResetTimers = async (extend = false) => { const method = extend ? 'POST' : 'GET'; - const headers = extend ? {} : { 'kbn-system-api': 'true' }; try { - const result = await this.http.fetch(SESSION_ROUTE, { method, headers }); + const result = await this.http.fetch(SESSION_ROUTE, { method, asSystemRequest: !extend }); this.handleSessionInfoAndResetTimers(result); diff --git a/x-pack/plugins/security/public/session/session_timeout_http_interceptor.test.ts b/x-pack/plugins/security/public/session/session_timeout_http_interceptor.test.ts index ffbd625590b158..427bdb04f9c61f 100644 --- a/x-pack/plugins/security/public/session/session_timeout_http_interceptor.test.ts +++ b/x-pack/plugins/security/public/session/session_timeout_http_interceptor.test.ts @@ -58,7 +58,7 @@ describe('response', () => { http.intercept(interceptor); fetchMock.mock('*', 200); - await http.fetch('/foo-api', { headers: { 'kbn-system-api': 'true' } }); + await http.fetch('/foo-api', { asSystemRequest: true }); expect(sessionTimeoutMock.extend).not.toHaveBeenCalled(); }); @@ -99,9 +99,9 @@ describe('responseError', () => { http.intercept(interceptor); fetchMock.mock('*', 401); - await expect( - http.fetch('/foo-api', { headers: { 'kbn-system-api': 'true' } }) - ).rejects.toMatchInlineSnapshot(`[Error: Unauthorized]`); + await expect(http.fetch('/foo-api', { asSystemRequest: true })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized]` + ); expect(sessionTimeoutMock.extend).not.toHaveBeenCalled(); }); diff --git a/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts b/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts index 8a2251f3f7f7c0..4033c04378a52f 100644 --- a/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts +++ b/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts @@ -6,38 +6,34 @@ import { HttpInterceptor, - HttpErrorResponse, - IHttpResponse, + HttpInterceptorResponseError, + HttpResponse, IAnonymousPaths, } from 'src/core/public'; import { ISessionTimeout } from './session_timeout'; -const isSystemAPIRequest = (request: Request) => { - return request.headers.has('kbn-system-api'); -}; - export class SessionTimeoutHttpInterceptor implements HttpInterceptor { constructor(private sessionTimeout: ISessionTimeout, private anonymousPaths: IAnonymousPaths) {} - response(httpResponse: IHttpResponse) { + response(httpResponse: HttpResponse) { if (this.anonymousPaths.isAnonymous(window.location.pathname)) { return; } - if (isSystemAPIRequest(httpResponse.request)) { + if (httpResponse.fetchOptions.asSystemRequest) { return; } this.sessionTimeout.extend(httpResponse.request.url); } - responseError(httpErrorResponse: HttpErrorResponse) { + responseError(httpErrorResponse: HttpInterceptorResponseError) { if (this.anonymousPaths.isAnonymous(window.location.pathname)) { return; } - if (isSystemAPIRequest(httpErrorResponse.request)) { + if (httpErrorResponse.fetchOptions.asSystemRequest) { return; } diff --git a/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.ts b/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.ts index a0ef2fdb86b47e..6b5eadcab74418 100644 --- a/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.ts +++ b/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.ts @@ -6,7 +6,7 @@ import { HttpInterceptor, - HttpErrorResponse, + HttpInterceptorResponseError, IHttpInterceptController, IAnonymousPaths, } from 'src/core/public'; @@ -16,7 +16,10 @@ import { SessionExpired } from './session_expired'; export class UnauthorizedResponseHttpInterceptor implements HttpInterceptor { constructor(private sessionExpired: SessionExpired, private anonymousPaths: IAnonymousPaths) {} - responseError(httpErrorResponse: HttpErrorResponse, controller: IHttpInterceptController) { + responseError( + httpErrorResponse: HttpInterceptorResponseError, + controller: IHttpInterceptController + ) { if (this.anonymousPaths.isAnonymous(window.location.pathname)) { return; }