From 834eaab59527b5bad210aaca412c79d2d8229e90 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Wed, 17 Jul 2019 10:56:13 +0200 Subject: [PATCH 01/15] add response error --- .../server/http/router/response_error.test.ts | 39 +++++++++++++++++ src/core/server/http/router/response_error.ts | 43 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 src/core/server/http/router/response_error.test.ts create mode 100644 src/core/server/http/router/response_error.ts diff --git a/src/core/server/http/router/response_error.test.ts b/src/core/server/http/router/response_error.test.ts new file mode 100644 index 00000000000000..5a29077f2c182e --- /dev/null +++ b/src/core/server/http/router/response_error.test.ts @@ -0,0 +1,39 @@ +/* + * 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 { createResponseError, ResponseError } from './response_error'; + +describe('createResponseError', () => { + it('creates ResponseError instance', async () => { + expect(createResponseError(new Error('reason'))).toBeInstanceOf(ResponseError); + }); + + it('inherits from Error', async () => { + expect(createResponseError(new Error('reason'))).toBeInstanceOf(Error); + }); + + it('stores additional data', async () => { + const error = createResponseError(new Error('reason'), { data: { key: 'value' } }); + expect(error.meta).toEqual({ data: { key: 'value' } }); + }); + + it('accept a string as error message', async () => { + expect(createResponseError('reason')).toBeInstanceOf(Error); + expect(createResponseError('reason').message).toBe('reason'); + }); +}); diff --git a/src/core/server/http/router/response_error.ts b/src/core/server/http/router/response_error.ts new file mode 100644 index 00000000000000..578a4bc0746468 --- /dev/null +++ b/src/core/server/http/router/response_error.ts @@ -0,0 +1,43 @@ +/* + * 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. + */ + +/** + * @public + */ +export interface ResponseErrorMeta { + data?: Record; + errorCode?: string; // error code to simplify search, translations in i18n, etc. + docLink?: string; // link to the docs +} + +/** + * @public + */ +export class ResponseError extends Error implements ResponseError { + constructor(error: Error | string, public readonly meta?: ResponseErrorMeta) { + super(typeof error === 'string' ? error : error.message); + Object.setPrototypeOf(this, ResponseError.prototype); + } +} + +/** + * @public + */ +export const createResponseError = (error: Error | string, meta?: ResponseErrorMeta) => + new ResponseError(error, meta); From e226236bfeb70d2ede0f8da4ab2c6e51991e84f2 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Wed, 17 Jul 2019 11:02:14 +0200 Subject: [PATCH 02/15] add hapi response adapter --- .../server/http/router/response_adapter.ts | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 src/core/server/http/router/response_adapter.ts diff --git a/src/core/server/http/router/response_adapter.ts b/src/core/server/http/router/response_adapter.ts new file mode 100644 index 00000000000000..8009a09d237c87 --- /dev/null +++ b/src/core/server/http/router/response_adapter.ts @@ -0,0 +1,102 @@ +/* + * 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 { ResponseObject, ResponseToolkit } from 'hapi'; +import typeDetect from 'type-detect'; + +import { HttpResponsePayload, KibanaResponse } from './response'; +import { ResponseError } from './response_error'; + +function setHeaders(response: ResponseObject, headers: Record = {}) { + Object.entries(headers).forEach(([header, value]) => { + if (value !== undefined) { + // Hapi typings for header accept only string, although string[] is a valid value + response.header(header, value as any); + } + }); + return response; +} + +const statusHelpers = { + isSuccess: (code: number) => code >= 100 && code < 300, + isRedirect: (code: number) => code >= 300 && code < 400, + isError: (code: number) => code >= 400 && code < 600, +}; + +export class HapiResponseAdapter { + constructor(private readonly responseToolkit: ResponseToolkit) {} + public toBadRequest(message: string) { + return this.responseToolkit.response({ error: message }).code(400); + } + + public toInternalError() { + return this.responseToolkit.response({ error: 'An internal server error occurred.' }).code(500); + } + + public handle(kibanaResponse: KibanaResponse) { + if (!(kibanaResponse instanceof KibanaResponse)) { + throw new Error( + `Unexpected result from Route Handler. Expected KibanaResponse, but given: ${typeDetect( + kibanaResponse + )}.` + ); + } + + const response = this.toHapiResponse(kibanaResponse); + setHeaders(response, kibanaResponse.options.headers); + return response; + } + + private toHapiResponse(kibanaResponse: KibanaResponse) { + if (statusHelpers.isSuccess(kibanaResponse.status)) { + return this.toSuccess(kibanaResponse); + } + if (statusHelpers.isRedirect(kibanaResponse.status)) { + return this.toRedirect(kibanaResponse); + } + if (statusHelpers.isError(kibanaResponse.status)) { + return this.toError(kibanaResponse); + } + throw new Error( + `Unexpected Http status code. Expected from 100 to 599, but given: ${kibanaResponse.status}.` + ); + } + + private toSuccess(kibanaResponse: KibanaResponse) { + return this.responseToolkit.response(kibanaResponse.payload).code(kibanaResponse.status); + } + + private toRedirect(kibanaResponse: KibanaResponse) { + const url = kibanaResponse.payload; + if (typeof url !== 'string') { + throw new Error(`expected redirection url, but given ${typeDetect(url)}`); + } + return this.responseToolkit.redirect(url).code(kibanaResponse.status); + } + + private toError(kibanaResponse: KibanaResponse) { + if (!(kibanaResponse.payload instanceof Error)) { + throw new Error(`expected Error object, but given ${typeDetect(kibanaResponse.payload)}`); + } + const payload = { + error: kibanaResponse.payload.message, + meta: kibanaResponse.payload.meta, + }; + return this.responseToolkit.response(payload).code(kibanaResponse.status); + } +} From 55a7abc8a93a15e0b0ed8eed684a94f8115d7cbe Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Wed, 17 Jul 2019 11:02:57 +0200 Subject: [PATCH 03/15] add router handler --- src/core/server/http/http_server.ts | 5 +- src/core/server/http/index.ts | 3 + src/core/server/http/router/index.ts | 1 + src/core/server/http/router/response.ts | 86 ++++++++++++++++++++++--- src/core/server/http/router/router.ts | 45 ++++++------- src/core/server/index.ts | 3 + 6 files changed, 106 insertions(+), 37 deletions(-) diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 4ca76d405a1fb2..d2372688c3324b 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Request, Server } from 'hapi'; +import { Request, Server, ResponseToolkit } from 'hapi'; import { Logger, LoggerFactory } from '../logging'; import { HttpConfig } from './http_config'; @@ -151,7 +151,8 @@ export class HttpServer { for (const route of router.getRoutes()) { const { authRequired = true, tags } = route.options; this.server.route({ - handler: route.handler, + handler: (req: Request, responseToolkit: ResponseToolkit) => + route.handler(req, responseToolkit, this.log), method: route.method, path: this.getRouteFullPath(router.path, route.path), options: { diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index e9c2425bc82cfd..f1c0857f96bbc0 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -21,9 +21,12 @@ export { config, HttpConfig, HttpConfigType } from './http_config'; export { HttpService, HttpServiceSetup, HttpServiceStart } from './http_service'; export { GetAuthHeaders } from './auth_headers_storage'; export { + createResponseError, isRealRequest, KibanaRequest, KibanaRequestRoute, + ResponseError, + ResponseErrorMeta, Router, RouteMethod, RouteConfigOptions, diff --git a/src/core/server/http/router/index.ts b/src/core/server/http/router/index.ts index 0a1c402917e45e..d0dc6d53086298 100644 --- a/src/core/server/http/router/index.ts +++ b/src/core/server/http/router/index.ts @@ -21,3 +21,4 @@ export { Headers, filterHeaders } from './headers'; export { Router } from './router'; export { KibanaRequest, KibanaRequestRoute, ensureRawRequest, isRealRequest } from './request'; export { RouteMethod, RouteConfigOptions } from './route'; +export { createResponseError, ResponseError, ResponseErrorMeta } from './response_error'; diff --git a/src/core/server/http/router/response.ts b/src/core/server/http/router/response.ts index 6e767aea0033de..33a7dde82d5e49 100644 --- a/src/core/server/http/router/response.ts +++ b/src/core/server/http/router/response.ts @@ -16,19 +16,89 @@ * specific language governing permissions and limitations * under the License. */ +import { IncomingHttpHeaders } from 'http'; +import { Stream } from 'stream'; -// TODO Needs _some_ work -export type StatusCode = 200 | 202 | 204 | 400; +import { ResponseError } from './response_error'; -export class KibanaResponse { - constructor(readonly status: StatusCode, readonly payload?: T) {} +export class KibanaResponse { + constructor( + readonly status: number, + readonly payload?: T, + readonly options: HttpResponseOptions = {} + ) {} +} + +type KnownKeys = { + [K in keyof T]: string extends K ? never : number extends K ? never : K; +} extends { [_ in keyof T]: infer U } + ? U + : never; + +type KnownHeaders = KnownKeys; +/** + * @public + */ +export interface HttpResponseOptions { + headers?: { [header in KnownHeaders]?: string | string[] } & { + [header: string]: string | string[]; + }; +} + +/** + * @public + */ +export type HttpResponsePayload = undefined | string | Record | Buffer | Stream; + +/** + * @public + */ +export interface CustomResponseOptions extends HttpResponseOptions { + statusCode: number; } export const responseFactory = { - accepted: (payload: T) => new KibanaResponse(202, payload), - badRequest: (err: T) => new KibanaResponse(400, err), - noContent: () => new KibanaResponse(204), - ok: (payload: T) => new KibanaResponse(200, payload), + // Success + ok: (payload: T, options: HttpResponseOptions = {}) => + new KibanaResponse(200, payload, options), + accepted: (payload?: T, options: HttpResponseOptions = {}) => + new KibanaResponse(202, payload, options), + noContent: (options: HttpResponseOptions = {}) => new KibanaResponse(204, undefined, options), + + custom: ( + payload: T, + options: CustomResponseOptions + ) => { + if (!options || !options.statusCode) { + throw new Error(`options.statusCode is expected to be set. given options: ${options}`); + } + const { statusCode: code, ...rest } = options; + return new KibanaResponse(code, payload, rest); + }, + + // Redirection + redirected: (url: string, options: HttpResponseOptions = {}) => + new KibanaResponse(302, url, options), + + // Client error + badRequest: (err: T, options: HttpResponseOptions = {}) => + new KibanaResponse(400, err, options), + unauthorized: (err: T, options: HttpResponseOptions = {}) => + new KibanaResponse(401, err, options), + + forbidden: (err: T, options: HttpResponseOptions = {}) => + new KibanaResponse(403, err, options), + notFound: (err: T, options: HttpResponseOptions = {}) => + new KibanaResponse(404, err, options), + conflict: (err: T, options: HttpResponseOptions = {}) => + new KibanaResponse(409, err, options), + + // Server error + internal: (err: T, options: HttpResponseOptions = {}) => + new KibanaResponse(500, err, options), }; +/** + * @public + */ export type ResponseFactory = typeof responseFactory; diff --git a/src/core/server/http/router/router.ts b/src/core/server/http/router/router.ts index 9d3295a7f3bf69..72cd37cc1ee471 100644 --- a/src/core/server/http/router/router.ts +++ b/src/core/server/http/router/router.ts @@ -20,15 +20,17 @@ import { ObjectType, schema, TypeOf } from '@kbn/config-schema'; import { Request, ResponseObject, ResponseToolkit } from 'hapi'; +import { Logger } from '../../logging'; import { KibanaRequest } from './request'; import { KibanaResponse, ResponseFactory, responseFactory } from './response'; import { RouteConfig, RouteConfigOptions, RouteMethod, RouteSchemas } from './route'; +import { HapiResponseAdapter } from './response_adapter'; export interface RouterRoute { method: RouteMethod; path: string; options: RouteConfigOptions; - handler: (req: Request, responseToolkit: ResponseToolkit) => Promise; + handler: (req: Request, responseToolkit: ResponseToolkit, log: Logger) => Promise; } /** @public */ @@ -47,8 +49,8 @@ export class Router { const { path, options = {} } = route; const routeSchemas = this.routeSchemasFromRouteConfig(route, 'get'); this.routes.push({ - handler: async (req, responseToolkit) => - await this.handle(routeSchemas, req, responseToolkit, handler), + handler: async (req, responseToolkit, log) => + await this.handle(routeSchemas, req, responseToolkit, handler, log), method: 'get', path, options, @@ -65,8 +67,8 @@ export class Router { const { path, options = {} } = route; const routeSchemas = this.routeSchemasFromRouteConfig(route, 'post'); this.routes.push({ - handler: async (req, responseToolkit) => - await this.handle(routeSchemas, req, responseToolkit, handler), + handler: async (req, responseToolkit, log) => + await this.handle(routeSchemas, req, responseToolkit, handler, log), method: 'post', path, options, @@ -83,8 +85,8 @@ export class Router { const { path, options = {} } = route; const routeSchemas = this.routeSchemasFromRouteConfig(route, 'put'); this.routes.push({ - handler: async (req, responseToolkit) => - await this.handle(routeSchemas, req, responseToolkit, handler), + handler: async (req, responseToolkit, log) => + await this.handle(routeSchemas, req, responseToolkit, handler, log), method: 'put', path, options, @@ -101,8 +103,8 @@ export class Router { const { path, options = {} } = route; const routeSchemas = this.routeSchemasFromRouteConfig(route, 'delete'); this.routes.push({ - handler: async (req, responseToolkit) => - await this.handle(routeSchemas, req, responseToolkit, handler), + handler: async (req, responseToolkit, log) => + await this.handle(routeSchemas, req, responseToolkit, handler, log), method: 'delete', path, options, @@ -143,34 +145,23 @@ export class Router { routeSchemas: RouteSchemas | undefined, request: Request, responseToolkit: ResponseToolkit, - handler: RequestHandler + handler: RequestHandler, + log: Logger ) { let kibanaRequest: KibanaRequest, TypeOf, TypeOf>; - + const hapiResponseAdapter = new HapiResponseAdapter(responseToolkit); try { kibanaRequest = KibanaRequest.from(request, routeSchemas); } catch (e) { - // TODO Handle failed validation - return responseToolkit.response({ error: e.message }).code(400); + return hapiResponseAdapter.toBadRequest(e.message); } try { const kibanaResponse = await handler(kibanaRequest, responseFactory); - - let payload = null; - if (kibanaResponse.payload instanceof Error) { - // TODO Design an error format - payload = { error: kibanaResponse.payload.message }; - } else if (kibanaResponse.payload !== undefined) { - payload = kibanaResponse.payload; - } - - return responseToolkit.response(payload).code(kibanaResponse.status); + return hapiResponseAdapter.handle(kibanaResponse); } catch (e) { - // TODO Handle `KibanaResponseError` - - // Otherwise we default to something along the lines of - return responseToolkit.response({ error: e.message }).code(500); + log.error(e); + return hapiResponseAdapter.toInternalError(); } } } diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 4582f1362922fc..065a5bbfdea891 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -64,12 +64,15 @@ export { AuthResultData, AuthToolkit, GetAuthHeaders, + createResponseError, KibanaRequest, KibanaRequestRoute, OnPreAuthHandler, OnPreAuthToolkit, OnPostAuthHandler, OnPostAuthToolkit, + ResponseError, + ResponseErrorMeta, Router, RouteMethod, RouteConfigOptions, From afd961a64fe09310d2f7401cdc1465e368ead3fb Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Wed, 17 Jul 2019 11:03:15 +0200 Subject: [PATCH 04/15] add tests --- src/core/server/http/http_server.test.ts | 81 -- src/core/server/http/router/response.test.ts | 879 +++++++++++++++++++ 2 files changed, 879 insertions(+), 81 deletions(-) create mode 100644 src/core/server/http/router/response.test.ts diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index aa341db20a6c97..51647ef1f74d11 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -102,87 +102,6 @@ Array [ `); }); -test('200 OK with body', async () => { - const router = new Router('/foo'); - - router.get({ path: '/', validate: false }, (req, res) => { - return res.ok({ key: 'value' }); - }); - - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); - await server.start(); - - await supertest(innerServer.listener) - .get('/foo/') - .expect(200) - .then(res => { - expect(res.body).toEqual({ key: 'value' }); - }); -}); - -test('202 Accepted with body', async () => { - const router = new Router('/foo'); - - router.get({ path: '/', validate: false }, (req, res) => { - return res.accepted({ location: 'somewhere' }); - }); - - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); - - await server.start(); - - await supertest(innerServer.listener) - .get('/foo/') - .expect(202) - .then(res => { - expect(res.body).toEqual({ location: 'somewhere' }); - }); -}); - -test('204 No content', async () => { - const router = new Router('/foo'); - - router.get({ path: '/', validate: false }, (req, res) => { - return res.noContent(); - }); - - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); - - await server.start(); - - await supertest(innerServer.listener) - .get('/foo/') - .expect(204) - .then(res => { - expect(res.body).toEqual({}); - // TODO Is ^ wrong or just a result of supertest, I expect `null` or `undefined` - }); -}); - -test('400 Bad request with error', async () => { - const router = new Router('/foo'); - - router.get({ path: '/', validate: false }, (req, res) => { - const err = new Error('some message'); - return res.badRequest(err); - }); - - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); - - await server.start(); - - await supertest(innerServer.listener) - .get('/foo/') - .expect(400) - .then(res => { - expect(res.body).toEqual({ error: 'some message' }); - }); -}); - test('valid params', async () => { const router = new Router('/foo'); diff --git a/src/core/server/http/router/response.test.ts b/src/core/server/http/router/response.test.ts new file mode 100644 index 00000000000000..83c2cb1a3c16af --- /dev/null +++ b/src/core/server/http/router/response.test.ts @@ -0,0 +1,879 @@ +/* + * 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 { Stream } from 'stream'; +import Boom from 'boom'; + +import supertest from 'supertest'; +import { ByteSizeValue } from '@kbn/config-schema'; + +import { HttpConfig, Router } from '..'; +import { createResponseError } from './response_error'; +import { loggingServiceMock } from '../../logging/logging_service.mock'; +import { LoggerFactory } from '../../logging/'; +import { HttpServer } from '../http_server'; + +let server: HttpServer; +let logger: LoggerFactory; + +const config = { + host: '127.0.0.1', + maxPayload: new ByteSizeValue(1024), + port: 10000, + ssl: { enabled: false }, +} as HttpConfig; + +beforeEach(() => { + logger = loggingServiceMock.create(); + server = new HttpServer(logger, 'tests'); +}); + +afterEach(async () => { + await server.stop(); +}); + +describe('Handler', () => { + it("Doesn't expose error details if handler throws", async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + throw new Error('unexpected error'); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body.error).toBe('An internal server error occurred.'); + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: unexpected error], + ], + ] + `); + }); + + it('returns 500 Server error if handler throws Boom error', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + throw Boom.unauthorized(); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body.error).toBe('An internal server error occurred.'); + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: Unauthorized], + ], + ] + `); + }); + + it('returns 500 Server error if handler returns unexpected result', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => 'ok' as any); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body.error).toBe('An internal server error occurred.'); + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: Unexpected result from Route Handler. Expected KibanaResponse, but given: string.], + ], + ] + `); + }); + + it('returns 400 Bad request if request validation failed', async () => { + const router = new Router('/'); + + router.get( + { + path: '/', + validate: schema => ({ + query: schema.object({ + page: schema.number(), + }), + }), + }, + (req, res) => res.noContent() + ); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .query({ page: 'one' }) + .expect(400); + + expect(result.body).toEqual({ + error: '[page]: expected value of type [number] but got [string]', + }); + }); +}); + +describe('Response factory', () => { + describe('Success', () => { + it('supports answering with json object', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.ok({ key: 'value' }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(200); + + expect(result.body).toEqual({ key: 'value' }); + expect(result.header['content-type']).toBe('application/json; charset=utf-8'); + }); + + it('supports answering with string', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.ok('result'); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(200); + + expect(result.text).toBe('result'); + expect(result.header['content-type']).toBe('text/html; charset=utf-8'); + }); + + it('supports answering with undefined', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.ok(undefined); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200); + }); + + it('supports answering with Stream', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const stream = new Stream.Readable({ + read() { + this.push('a'); + this.push('b'); + this.push('c'); + this.push(null); + }, + }); + + return res.ok(stream); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(200); + + expect(result.text).toBe('abc'); + expect(result.header['content-type']).toBe(undefined); // ? + }); + + it('supports answering with chunked Stream', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const stream = new Stream.PassThrough(); + stream.write('a'); + stream.write('b'); + setTimeout(function() { + stream.write('c'); + stream.end(); + }, 100); + + return res.ok(stream); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(200); + + expect(result.text).toBe('abc'); + expect(result.header['transfer-encoding']).toBe('chunked'); + }); + + it('supports answering with Buffer', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const buffer = Buffer.alloc(1028, '.'); + + return res.ok(buffer, { + headers: { + 'content-encoding': 'binary', + }, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(200) + .buffer(true); + + expect(result.header['content-encoding']).toBe('binary'); + expect(result.header['content-length']).toBe('1028'); + expect(result.header['content-type']).toBe('application/octet-stream'); + }); + + it('supports answering with Buffer text', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const buffer = new Buffer('abc'); + + return res.ok(buffer, { + headers: { + 'content-type': 'text/plain', + }, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(200) + .buffer(true); + + expect(result.text).toBe('abc'); + expect(result.header['content-length']).toBe('3'); + expect(result.header['content-type']).toBe('text/plain; charset=utf-8'); + }); + + it('supports configuring standard headers', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.ok('value', { + headers: { + etag: '1234', + }, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(200); + + expect(result.text).toEqual('value'); + expect(result.header.etag).toBe('1234'); + }); + + it('supports configuring non-standard headers', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.ok('value', { + headers: { + etag: '1234', + 'x-kibana': 'key', + }, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(200); + + expect(result.text).toEqual('value'); + expect(result.header.etag).toBe('1234'); + expect(result.header['x-kibana']).toBe('key'); + }); + + it('accepted headers are case-insensitive.', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.ok('value', { + headers: { + ETag: '1234', + }, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(200); + + expect(result.header.etag).toBe('1234'); + }); + + it('accept array of headers', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.ok('value', { + headers: { + 'set-cookie': ['foo', 'bar'], + }, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(200); + + expect(result.header['set-cookie']).toEqual(['foo', 'bar']); + }); + + it('throws if given invalid json object', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const payload: any = { key: {} }; + payload.key.payload = payload; + return res.ok(payload); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(500); + + // error happens within hapi when route handler already finished execution. + expect(loggingServiceMock.collect(logger).error).toHaveLength(0); + }); + + it('200 OK with body', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.ok({ key: 'value' }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(200); + + expect(result.body).toEqual({ key: 'value' }); + expect(result.header['content-type']).toBe('application/json; charset=utf-8'); + }); + + it('202 Accepted with body', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.accepted({ location: 'somewhere' }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(202); + + expect(result.body).toEqual({ location: 'somewhere' }); + expect(result.header['content-type']).toBe('application/json; charset=utf-8'); + }); + + it('204 No content', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.noContent(); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(204); + + expect(result.noContent).toBe(true); + }); + }); + + describe('Redirection', () => { + it('302 supports redirection to configured URL', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.redirected('/new-url', { + headers: { 'x-kibana': 'tag' }, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(302); + + expect(result.header.location).toBe('/new-url'); + expect(result.header['x-kibana']).toBe('tag'); + }); + + it('throws if redirection url not provided', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + // @ts-ignore url string is required parameter + return res.redirected(); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body.error).toBe('An internal server error occurred.'); + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: expected redirection url, but given undefined], + ], + ] + `); + }); + }); + + describe('Error', () => { + it('400 Bad request', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const error = new Error('some message'); + return res.badRequest(error); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(400); + + expect(result.body).toEqual({ error: 'some message' }); + }); + + it('400 Bad request with additional data', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const error = createResponseError('some message', { data: ['good', 'bad'] }); + return res.badRequest(error); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(400); + + expect(result.body).toEqual({ + error: 'some message', + meta: { + data: ['good', 'bad'], + }, + }); + }); + + it('401 Unauthorized', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const error = new Error('no access'); + return res.unauthorized(error, { + headers: { + 'WWW-Authenticate': 'challenge', + }, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(401); + + expect(result.body.error).toBe('no access'); + expect(result.header['www-authenticate']).toBe('challenge'); + }); + + it('403 Forbidden', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const error = new Error('reason'); + return res.forbidden(error); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(403); + + expect(result.body.error).toBe('reason'); + }); + + it('404 Not Found', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const error = new Error('file is not found'); + return res.notFound(error); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(404); + + expect(result.body.error).toBe('file is not found'); + }); + + it('409 Conflict', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const error = new Error('stale version'); + return res.conflict(error); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(409); + + expect(result.body.error).toBe('stale version'); + }); + }); + + describe('Custom', () => { + it('creates success response', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.custom(undefined, { + statusCode: 201, + headers: { + location: 'somewhere', + }, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(201); + + expect(result.header.location).toBe('somewhere'); + }); + + it('creates redirect response', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.custom('/new-url', { + statusCode: 301, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(301); + + expect(result.header.location).toBe('/new-url'); + }); + + it('creates error response', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const error = new Error('unauthorized'); + return res.custom(error, { + statusCode: 401, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(401); + + expect(result.body.error).toBe('unauthorized'); + }); + + it('creates error response with additional data', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const error = createResponseError('unauthorized', { errorCode: 'K401' }); + return res.custom(error, { + statusCode: 401, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(401); + + expect(result.body).toEqual({ + error: 'unauthorized', + meta: { errorCode: 'K401' }, + }); + }); + + it('creates error response with Boom error', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const error = Boom.unauthorized(); + return res.custom(error, { + statusCode: 401, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(401); + + expect(result.body.error).toBe('Unauthorized'); + }); + + it("Doesn't log details of created 500 Server error response", async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const error = createResponseError('reason'); + return res.custom(error, { + statusCode: 500, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body.error).toBe('reason'); + expect(loggingServiceMock.collect(logger).error).toHaveLength(0); + }); + + it('throws an error if not valid error is provided', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.custom( + { message: 'error-message' }, + { + statusCode: 401, + } + ); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body.error).toBe('An internal server error occurred.'); + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: expected Error object, but given Object], + ], + ] + `); + }); + + it('throws an error if statusCode is not specified', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const error = new Error('error message'); + // @ts-ignore options.statusCode is required + return res.custom(error); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body.error).toBe('An internal server error occurred.'); + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: options.statusCode is expected to be set. given options: undefined], + ], + ] + `); + }); + + it('throws an error if statusCode is not valid', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const error = new Error('error message'); + return res.custom(error, { statusCode: 20 }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body.error).toBe('An internal server error occurred.'); + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: Unexpected Http status code. Expected from 100 to 599, but given: 20.], + ], + ] + `); + }); + }); +}); From b0b888532c8a6930e707e13effec5140fea8bf64 Mon Sep 17 00:00:00 2001 From: restrry Date: Wed, 24 Jul 2019 15:26:59 +0200 Subject: [PATCH 05/15] add comments, update docs --- .../kibana-plugin-server.coresetup.http.md | 1 + .../server/kibana-plugin-server.coresetup.md | 2 +- ...ibana-plugin-server.createresponseerror.md | 13 +++ .../core/server/kibana-plugin-server.md | 8 ++ ...ugin-server.responseerror.(constructor).md | 21 ++++ .../kibana-plugin-server.responseerror.md | 26 +++++ ...kibana-plugin-server.responseerror.meta.md | 11 +++ ...na-plugin-server.responseerrormeta.data.md | 11 +++ ...plugin-server.responseerrormeta.doclink.md | 11 +++ ...ugin-server.responseerrormeta.errorcode.md | 11 +++ .../kibana-plugin-server.responseerrormeta.md | 22 +++++ src/core/server/http/router/response.ts | 99 ++++++++++++++++--- src/core/server/http/router/response_error.ts | 11 ++- src/core/server/http/router/router.ts | 2 +- src/core/server/index.ts | 1 + src/core/server/plugins/plugin_context.ts | 1 + src/core/server/server.api.md | 23 +++++ 17 files changed, 256 insertions(+), 18 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-server.createresponseerror.md create mode 100644 docs/development/core/server/kibana-plugin-server.responseerror.(constructor).md create mode 100644 docs/development/core/server/kibana-plugin-server.responseerror.md create mode 100644 docs/development/core/server/kibana-plugin-server.responseerror.meta.md create mode 100644 docs/development/core/server/kibana-plugin-server.responseerrormeta.data.md create mode 100644 docs/development/core/server/kibana-plugin-server.responseerrormeta.doclink.md create mode 100644 docs/development/core/server/kibana-plugin-server.responseerrormeta.errorcode.md create mode 100644 docs/development/core/server/kibana-plugin-server.responseerrormeta.md diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.http.md b/docs/development/core/server/kibana-plugin-server.coresetup.http.md index c9206b7a7e711c..379c53f34d3d40 100644 --- a/docs/development/core/server/kibana-plugin-server.coresetup.http.md +++ b/docs/development/core/server/kibana-plugin-server.coresetup.http.md @@ -12,6 +12,7 @@ http: { registerOnPreAuth: HttpServiceSetup['registerOnPreAuth']; registerAuth: HttpServiceSetup['registerAuth']; registerOnPostAuth: HttpServiceSetup['registerOnPostAuth']; + registerRouter: HttpServiceSetup['registerRouter']; basePath: HttpServiceSetup['basePath']; createNewServer: HttpServiceSetup['createNewServer']; isTlsEnabled: HttpServiceSetup['isTlsEnabled']; diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.md b/docs/development/core/server/kibana-plugin-server.coresetup.md index f4653d7f435798..2d34d4288bfe29 100644 --- a/docs/development/core/server/kibana-plugin-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-server.coresetup.md @@ -17,5 +17,5 @@ export interface CoreSetup | Property | Type | Description | | --- | --- | --- | | [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | {
adminClient$: Observable<ClusterClient>;
dataClient$: Observable<ClusterClient>;
createClient: (type: string, clientConfig?: Partial<ElasticsearchClientConfig>) => ClusterClient;
} | | -| [http](./kibana-plugin-server.coresetup.http.md) | {
createCookieSessionStorageFactory: HttpServiceSetup['createCookieSessionStorageFactory'];
registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];
registerAuth: HttpServiceSetup['registerAuth'];
registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];
basePath: HttpServiceSetup['basePath'];
createNewServer: HttpServiceSetup['createNewServer'];
isTlsEnabled: HttpServiceSetup['isTlsEnabled'];
} | | +| [http](./kibana-plugin-server.coresetup.http.md) | {
createCookieSessionStorageFactory: HttpServiceSetup['createCookieSessionStorageFactory'];
registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];
registerAuth: HttpServiceSetup['registerAuth'];
registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];
registerRouter: HttpServiceSetup['registerRouter'];
basePath: HttpServiceSetup['basePath'];
createNewServer: HttpServiceSetup['createNewServer'];
isTlsEnabled: HttpServiceSetup['isTlsEnabled'];
} | | diff --git a/docs/development/core/server/kibana-plugin-server.createresponseerror.md b/docs/development/core/server/kibana-plugin-server.createresponseerror.md new file mode 100644 index 00000000000000..65dcda022ecae2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.createresponseerror.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [createResponseError](./kibana-plugin-server.createresponseerror.md) + +## createResponseError variable + +Creates Kibana ResponseError instance + +Signature: + +```typescript +createResponseError: (error: string | Error, meta?: ResponseErrorMeta | undefined) => ResponseError +``` diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index ab79f2b3829094..c046ce256c9996 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -17,6 +17,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ClusterClient](./kibana-plugin-server.clusterclient.md) | Represents an Elasticsearch cluster API client and allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)). | | [ElasticsearchErrorHelpers](./kibana-plugin-server.elasticsearcherrorhelpers.md) | Helpers for working with errors returned from the Elasticsearch service.Since the internal data of errors are subject to change, consumers of the Elasticsearch service should always use these helpers to classify errors instead of checking error internals such as body.error.header[WWW-Authenticate] | | [KibanaRequest](./kibana-plugin-server.kibanarequest.md) | Kibana specific abstraction for an incoming request. | +| [ResponseError](./kibana-plugin-server.responseerror.md) | Kibana ResponseError object to store error details | | [Router](./kibana-plugin-server.router.md) | | | [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | | | [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md) | Serves the same purpose as "normal" ClusterClient but exposes additional callAsCurrentUser method that doesn't use credentials of the Kibana internal user (as callAsInternalUser does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API | @@ -47,6 +48,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. | | [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) | | | [PluginsServiceStart](./kibana-plugin-server.pluginsservicestart.md) | | +| [ResponseErrorMeta](./kibana-plugin-server.responseerrormeta.md) | Additional meta-data to enhance error output or provide error details. | | [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md) | Route specific configuration. | | [SavedObject](./kibana-plugin-server.savedobject.md) | | | [SavedObjectAttributes](./kibana-plugin-server.savedobjectattributes.md) | | @@ -66,6 +68,12 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SessionStorage](./kibana-plugin-server.sessionstorage.md) | Provides an interface to store and retrieve data across requests. | | [SessionStorageFactory](./kibana-plugin-server.sessionstoragefactory.md) | SessionStorage factory to bind one to an incoming request | +## Variables + +| Variable | Description | +| --- | --- | +| [createResponseError](./kibana-plugin-server.createresponseerror.md) | Creates Kibana ResponseError instance | + ## Type Aliases | Type Alias | Description | diff --git a/docs/development/core/server/kibana-plugin-server.responseerror.(constructor).md b/docs/development/core/server/kibana-plugin-server.responseerror.(constructor).md new file mode 100644 index 00000000000000..d148bfca478715 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.responseerror.(constructor).md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ResponseError](./kibana-plugin-server.responseerror.md) > [(constructor)](./kibana-plugin-server.responseerror.(constructor).md) + +## ResponseError.(constructor) + +Constructs a new instance of the `ResponseError` class + +Signature: + +```typescript +constructor(error: Error | string, meta?: ResponseErrorMeta | undefined); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | string | | +| meta | ResponseErrorMeta | undefined | | + diff --git a/docs/development/core/server/kibana-plugin-server.responseerror.md b/docs/development/core/server/kibana-plugin-server.responseerror.md new file mode 100644 index 00000000000000..9182594f5366c0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.responseerror.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ResponseError](./kibana-plugin-server.responseerror.md) + +## ResponseError class + +Kibana ResponseError object to store error details + +Signature: + +```typescript +export declare class ResponseError extends Error +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(error, meta)](./kibana-plugin-server.responseerror.(constructor).md) | | Constructs a new instance of the ResponseError class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [meta](./kibana-plugin-server.responseerror.meta.md) | | ResponseErrorMeta | undefined | | + diff --git a/docs/development/core/server/kibana-plugin-server.responseerror.meta.md b/docs/development/core/server/kibana-plugin-server.responseerror.meta.md new file mode 100644 index 00000000000000..0c020730c00b5a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.responseerror.meta.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ResponseError](./kibana-plugin-server.responseerror.md) > [meta](./kibana-plugin-server.responseerror.meta.md) + +## ResponseError.meta property + +Signature: + +```typescript +readonly meta?: ResponseErrorMeta | undefined; +``` diff --git a/docs/development/core/server/kibana-plugin-server.responseerrormeta.data.md b/docs/development/core/server/kibana-plugin-server.responseerrormeta.data.md new file mode 100644 index 00000000000000..afef0c88432a48 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.responseerrormeta.data.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ResponseErrorMeta](./kibana-plugin-server.responseerrormeta.md) > [data](./kibana-plugin-server.responseerrormeta.data.md) + +## ResponseErrorMeta.data property + +Signature: + +```typescript +data?: Record; +``` diff --git a/docs/development/core/server/kibana-plugin-server.responseerrormeta.doclink.md b/docs/development/core/server/kibana-plugin-server.responseerrormeta.doclink.md new file mode 100644 index 00000000000000..472cb3ef48e36d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.responseerrormeta.doclink.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ResponseErrorMeta](./kibana-plugin-server.responseerrormeta.md) > [docLink](./kibana-plugin-server.responseerrormeta.doclink.md) + +## ResponseErrorMeta.docLink property + +Signature: + +```typescript +docLink?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.responseerrormeta.errorcode.md b/docs/development/core/server/kibana-plugin-server.responseerrormeta.errorcode.md new file mode 100644 index 00000000000000..1f26f072e0b9af --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.responseerrormeta.errorcode.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ResponseErrorMeta](./kibana-plugin-server.responseerrormeta.md) > [errorCode](./kibana-plugin-server.responseerrormeta.errorcode.md) + +## ResponseErrorMeta.errorCode property + +Signature: + +```typescript +errorCode?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.responseerrormeta.md b/docs/development/core/server/kibana-plugin-server.responseerrormeta.md new file mode 100644 index 00000000000000..81965704dd1766 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.responseerrormeta.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ResponseErrorMeta](./kibana-plugin-server.responseerrormeta.md) + +## ResponseErrorMeta interface + +Additional meta-data to enhance error output or provide error details. + +Signature: + +```typescript +export interface ResponseErrorMeta +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [data](./kibana-plugin-server.responseerrormeta.data.md) | Record<string, any> | | +| [docLink](./kibana-plugin-server.responseerrormeta.doclink.md) | string | | +| [errorCode](./kibana-plugin-server.responseerrormeta.errorcode.md) | string | | + diff --git a/src/core/server/http/router/response.ts b/src/core/server/http/router/response.ts index 33a7dde82d5e49..1062002374d2f9 100644 --- a/src/core/server/http/router/response.ts +++ b/src/core/server/http/router/response.ts @@ -37,9 +37,11 @@ type KnownKeys = { type KnownHeaders = KnownKeys; /** + * HTTP response parameters * @public */ export interface HttpResponseOptions { + /** HTTP Headers with additional information about response */ headers?: { [header in KnownHeaders]?: string | string[] } & { [header: string]: string | string[]; }; @@ -51,6 +53,7 @@ export interface HttpResponseOptions { export type HttpResponsePayload = undefined | string | Record | Buffer | Stream; /** + * HTTP response parameters * @public */ export interface CustomResponseOptions extends HttpResponseOptions { @@ -59,12 +62,36 @@ export interface CustomResponseOptions extends HttpResponseOptions { export const responseFactory = { // Success + /** + * The request has succeeded. + * Status code: `200`. + * @param payload - {@link HttpResponsePayload} payload to send to the client + * @param options - {@link HttpResponseOptions} configures HTTP response parameters. + */ ok: (payload: T, options: HttpResponseOptions = {}) => new KibanaResponse(200, payload, options), + + /** + * The request has been accepted for processing. + * Status code: `202`. + * @param payload - {@link HttpResponsePayload} payload to send to the client + * @param options - {@link HttpResponseOptions} configures HTTP response parameters. + */ accepted: (payload?: T, options: HttpResponseOptions = {}) => new KibanaResponse(202, payload, options), + + /** + * The server has successfully fulfilled the request and that there is no additional content to send in the response payload body. + * Status code: `204`. + * @param options - {@link HttpResponseOptions} configures HTTP response parameters. + */ noContent: (options: HttpResponseOptions = {}) => new KibanaResponse(204, undefined, options), + /** + * Creates a response with defined status code and payload. + * @param payload - {@link HttpResponsePayload} payload to send to the client + * @param options - {@link CustomResponseOptions} configures HTTP response parameters. + */ custom: ( payload: T, options: CustomResponseOptions @@ -77,28 +104,74 @@ export const responseFactory = { }, // Redirection + /** + * Redirect to a different URI. + * Status code: `302`. + * @param url - an absolute or relative URI used to redirect the client to another resource. + * @param options - {@link HttpResponseOptions} configures HTTP response parameters. + */ redirected: (url: string, options: HttpResponseOptions = {}) => new KibanaResponse(302, url, options), // Client error - badRequest: (err: T, options: HttpResponseOptions = {}) => - new KibanaResponse(400, err, options), - unauthorized: (err: T, options: HttpResponseOptions = {}) => - new KibanaResponse(401, err, options), - - forbidden: (err: T, options: HttpResponseOptions = {}) => - new KibanaResponse(403, err, options), - notFound: (err: T, options: HttpResponseOptions = {}) => - new KibanaResponse(404, err, options), - conflict: (err: T, options: HttpResponseOptions = {}) => - new KibanaResponse(409, err, options), + /** + * The server cannot process the request due to something that is perceived to be a client error. + * Status code: `400`. + * @param error - {@link ResponseError} Error object containing message and other error details to pass to the client + * @param options - {@link HttpResponseOptions} configures HTTP response parameters. + */ + badRequest: (error: T, options: HttpResponseOptions = {}) => + new KibanaResponse(400, error, options), + + /** + * The request cannot be applied because it lacks valid authentication credentials for the target resource. + * Status code: `401`. + * @param error - {@link ResponseError} Error object containing message and other error details to pass to the client + * @param options - {@link HttpResponseOptions} configures HTTP response parameters. + */ + unauthorized: (error: T, options: HttpResponseOptions = {}) => + new KibanaResponse(401, error, options), + + /** + * Server cannot grant access to a resource. + * Status code: `403`. + * @param error - {@link ResponseError} Error object containing message and other error details to pass to the client + * @param options - {@link HttpResponseOptions} configures HTTP response parameters. + */ + forbidden: (error: T, options: HttpResponseOptions = {}) => + new KibanaResponse(403, error, options), + + /** + * Server cannot find a current representation for the target resource. + * Status code: `404`. + * @param error - {@link ResponseError} Error object containing message and other error details to pass to the client + * @param options - {@link HttpResponseOptions} configures HTTP response parameters. + */ + notFound: (error: T, options: HttpResponseOptions = {}) => + new KibanaResponse(404, error, options), + + /** + * The request could not be completed due to a conflict with the current state of the target resource. + * Status code: `409`. + * @param error - {@link ResponseError} Error object containing message and other error details to pass to the client + * @param options - {@link HttpResponseOptions} configures HTTP response parameters. + */ + conflict: (error: T, options: HttpResponseOptions = {}) => + new KibanaResponse(409, error, options), // Server error - internal: (err: T, options: HttpResponseOptions = {}) => - new KibanaResponse(500, err, options), + /** + * The server encountered an unexpected condition that prevented it from fulfilling the request. + * Status code: `500`. + * @param error - {@link ResponseError} Error object containing message and other error details to pass to the client + * @param options - {@link HttpResponseOptions} configures HTTP response parameters. + */ + internal: (error: T, options: HttpResponseOptions = {}) => + new KibanaResponse(500, error, options), }; /** + * Creates an object containing request response payload, HTTP headers, error details, and other data transmitted to the client. * @public */ export type ResponseFactory = typeof responseFactory; diff --git a/src/core/server/http/router/response_error.ts b/src/core/server/http/router/response_error.ts index 578a4bc0746468..cd19a7a57b4601 100644 --- a/src/core/server/http/router/response_error.ts +++ b/src/core/server/http/router/response_error.ts @@ -18,18 +18,20 @@ */ /** + * Additional meta-data to enhance error output or provide error details. * @public */ export interface ResponseErrorMeta { data?: Record; - errorCode?: string; // error code to simplify search, translations in i18n, etc. - docLink?: string; // link to the docs + errorCode?: string; + docLink?: string; } /** + * Kibana ResponseError object to store error details * @public */ -export class ResponseError extends Error implements ResponseError { +export class ResponseError extends Error { constructor(error: Error | string, public readonly meta?: ResponseErrorMeta) { super(typeof error === 'string' ? error : error.message); Object.setPrototypeOf(this, ResponseError.prototype); @@ -37,6 +39,9 @@ export class ResponseError extends Error implements ResponseError { } /** + * Creates Kibana ResponseError instance + * @param error - Error object or message to wrap + * @param meta - additional meta-data to pass to the client * @public */ export const createResponseError = (error: Error | string, meta?: ResponseErrorMeta) => diff --git a/src/core/server/http/router/router.ts b/src/core/server/http/router/router.ts index 72cd37cc1ee471..000891ede57a9e 100644 --- a/src/core/server/http/router/router.ts +++ b/src/core/server/http/router/router.ts @@ -167,6 +167,6 @@ export class Router { } export type RequestHandler

= ( - req: KibanaRequest, TypeOf, TypeOf>, + request: KibanaRequest, TypeOf, TypeOf>, createResponse: ResponseFactory ) => KibanaResponse | Promise>; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 065a5bbfdea891..43167ba4d2cb87 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -132,6 +132,7 @@ export interface CoreSetup { registerOnPreAuth: HttpServiceSetup['registerOnPreAuth']; registerAuth: HttpServiceSetup['registerAuth']; registerOnPostAuth: HttpServiceSetup['registerOnPostAuth']; + registerRouter: HttpServiceSetup['registerRouter']; basePath: HttpServiceSetup['basePath']; createNewServer: HttpServiceSetup['createNewServer']; isTlsEnabled: HttpServiceSetup['isTlsEnabled']; diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index fcc8a26f51b4b0..4ec489ee880cec 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -122,6 +122,7 @@ export function createPluginSetupContext( registerOnPreAuth: deps.http.registerOnPreAuth, registerAuth: deps.http.registerAuth, registerOnPostAuth: deps.http.registerOnPostAuth, + registerRouter: deps.http.registerRouter, basePath: deps.http.basePath, createNewServer: deps.http.createNewServer, isTlsEnabled: deps.http.isTlsEnabled, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index a6fbfebf9d9470..c1ab562f8b33ec 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -9,6 +9,7 @@ import { ByteSizeValue } from '@kbn/config-schema'; import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; import { ConfigOptions } from 'elasticsearch'; import { Duration } from 'moment'; +import { IncomingHttpHeaders } from 'http'; import { ObjectType } from '@kbn/config-schema'; import { Observable } from 'rxjs'; import { Request } from 'hapi'; @@ -16,6 +17,7 @@ import { ResponseObject } from 'hapi'; import { ResponseToolkit } from 'hapi'; import { Schema } from '@kbn/config-schema'; import { Server } from 'hapi'; +import { Stream } from 'stream'; import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; import { Url } from 'url'; @@ -97,6 +99,7 @@ export interface CoreSetup { registerOnPreAuth: HttpServiceSetup['registerOnPreAuth']; registerAuth: HttpServiceSetup['registerAuth']; registerOnPostAuth: HttpServiceSetup['registerOnPostAuth']; + registerRouter: HttpServiceSetup['registerRouter']; basePath: HttpServiceSetup['basePath']; createNewServer: HttpServiceSetup['createNewServer']; isTlsEnabled: HttpServiceSetup['isTlsEnabled']; @@ -107,6 +110,9 @@ export interface CoreSetup { export interface CoreStart { } +// @public +export const createResponseError: (error: string | Error, meta?: ResponseErrorMeta | undefined) => ResponseError; + // @public export interface DiscoveredPlugin { readonly configPath: ConfigPath; @@ -385,6 +391,23 @@ export type RecursiveReadonly = T extends (...args: any[]) => any ? T : T ext [K in keyof T]: RecursiveReadonly; }> : T; +// @public +export class ResponseError extends Error { + constructor(error: Error | string, meta?: ResponseErrorMeta | undefined); + // (undocumented) + readonly meta?: ResponseErrorMeta | undefined; +} + +// @public +export interface ResponseErrorMeta { + // (undocumented) + data?: Record; + // (undocumented) + docLink?: string; + // (undocumented) + errorCode?: string; +} + // @public export interface RouteConfigOptions { authRequired?: boolean; From 61800610c36f6cfc316a58257e32a57bf06a0fec Mon Sep 17 00:00:00 2001 From: restrry Date: Thu, 25 Jul 2019 15:21:08 +0200 Subject: [PATCH 06/15] update tests --- src/core/server/http/router/response.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/server/http/router/response.test.ts b/src/core/server/http/router/response.test.ts index 83c2cb1a3c16af..ae7d2b83c1e2b4 100644 --- a/src/core/server/http/router/response.test.ts +++ b/src/core/server/http/router/response.test.ts @@ -20,7 +20,7 @@ import { Stream } from 'stream'; import Boom from 'boom'; import supertest from 'supertest'; -import { ByteSizeValue } from '@kbn/config-schema'; +import { ByteSizeValue, schema } from '@kbn/config-schema'; import { HttpConfig, Router } from '..'; import { createResponseError } from './response_error'; @@ -127,11 +127,11 @@ describe('Handler', () => { router.get( { path: '/', - validate: schema => ({ + validate: { query: schema.object({ page: schema.number(), }), - }), + }, }, (req, res) => res.noContent() ); @@ -146,7 +146,7 @@ describe('Handler', () => { .expect(400); expect(result.body).toEqual({ - error: '[page]: expected value of type [number] but got [string]', + error: '[request query.page]: expected value of type [number] but got [string]', }); }); }); @@ -232,7 +232,7 @@ describe('Response factory', () => { .expect(200); expect(result.text).toBe('abc'); - expect(result.header['content-type']).toBe(undefined); // ? + expect(result.header['content-type']).toBe(undefined); }); it('supports answering with chunked Stream', async () => { From 92fe667913a6c05132d23aae6d60f77c7c414249 Mon Sep 17 00:00:00 2001 From: restrry Date: Thu, 25 Jul 2019 15:30:30 +0200 Subject: [PATCH 07/15] cleanup tests --- src/core/server/http/router/response.test.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/core/server/http/router/response.test.ts b/src/core/server/http/router/response.test.ts index ae7d2b83c1e2b4..71c23f26494bae 100644 --- a/src/core/server/http/router/response.test.ts +++ b/src/core/server/http/router/response.test.ts @@ -408,7 +408,7 @@ describe('Response factory', () => { expect(result.header['set-cookie']).toEqual(['foo', 'bar']); }); - it('throws if given invalid json object', async () => { + it('throws if given invalid json object as response payload', async () => { const router = new Router('/'); router.get({ path: '/', validate: false }, (req, res) => { @@ -512,8 +512,7 @@ describe('Response factory', () => { const router = new Router('/'); router.get({ path: '/', validate: false }, (req, res) => { - // @ts-ignore url string is required parameter - return res.redirected(); + return res.redirected(undefined as any); // url string is required parameter }); const { registerRouter, server: innerServer } = await server.setup(config); @@ -828,8 +827,7 @@ describe('Response factory', () => { router.get({ path: '/', validate: false }, (req, res) => { const error = new Error('error message'); - // @ts-ignore options.statusCode is required - return res.custom(error); + return res.custom(error, undefined as any); // options.statusCode is required }); const { registerRouter, server: innerServer } = await server.setup(config); From 9348bae71ef6f32670ec8a8ae68488bb603f7ba8 Mon Sep 17 00:00:00 2001 From: restrry Date: Mon, 29 Jul 2019 10:25:54 +0200 Subject: [PATCH 08/15] address @joshdover comments --- src/core/server/http/router/response.test.ts | 48 +++++++++++++++++-- src/core/server/http/router/response.ts | 31 ++++++++++-- .../server/http/router/response_adapter.ts | 20 ++++---- src/core/server/http/router/response_error.ts | 2 + src/core/server/index.ts | 1 - src/core/server/plugins/plugin_context.ts | 1 - 6 files changed, 84 insertions(+), 19 deletions(-) diff --git a/src/core/server/http/router/response.test.ts b/src/core/server/http/router/response.test.ts index 71c23f26494bae..1f09f37f55174e 100644 --- a/src/core/server/http/router/response.test.ts +++ b/src/core/server/http/router/response.test.ts @@ -491,8 +491,11 @@ describe('Response factory', () => { const router = new Router('/'); router.get({ path: '/', validate: false }, (req, res) => { - return res.redirected('/new-url', { - headers: { 'x-kibana': 'tag' }, + return res.redirected('The document has moved', { + headers: { + location: '/new-url', + 'x-kibana': 'tag', + }, }); }); @@ -504,6 +507,7 @@ describe('Response factory', () => { .get('/') .expect(302); + expect(result.text).toBe('The document has moved'); expect(result.header.location).toBe('/new-url'); expect(result.header['x-kibana']).toBe('tag'); }); @@ -512,7 +516,11 @@ describe('Response factory', () => { const router = new Router('/'); router.get({ path: '/', validate: false }, (req, res) => { - return res.redirected(undefined as any); // url string is required parameter + return res.redirected(undefined, { + headers: { + 'x-kibana': 'tag', + }, + } as any); // location headers is required }); const { registerRouter, server: innerServer } = await server.setup(config); @@ -527,7 +535,7 @@ describe('Response factory', () => { expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` Array [ Array [ - [Error: expected redirection url, but given undefined], + [Error: expected 'location' header to be set], ], ] `); @@ -688,7 +696,10 @@ describe('Response factory', () => { const router = new Router('/'); router.get({ path: '/', validate: false }, (req, res) => { - return res.custom('/new-url', { + return res.custom('The document has moved', { + headers: { + location: '/new-url', + }, statusCode: 301, }); }); @@ -704,6 +715,33 @@ describe('Response factory', () => { expect(result.header.location).toBe('/new-url'); }); + it('throws if redirects without location header to be set', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.custom('The document has moved', { + headers: {}, + statusCode: 301, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: expected 'location' header to be set], + ], + ] + `); + }); + it('creates error response', async () => { const router = new Router('/'); diff --git a/src/core/server/http/router/response.ts b/src/core/server/http/router/response.ts index 1062002374d2f9..930674c9cdc195 100644 --- a/src/core/server/http/router/response.ts +++ b/src/core/server/http/router/response.ts @@ -29,6 +29,18 @@ export class KibanaResponse { ) {} } +/** + * Creates a Union type of all known keys of a given interface. + * @example + * ```ts + * interface Person { + * name: string; + * age: number; + * [attributes: string]: string | number; + * } + * type PersonKnownKeys = KnownKeys; // "age" | "name" + * ``` + */ type KnownKeys = { [K in keyof T]: string extends K ? never : number extends K ? never : K; } extends { [_ in keyof T]: infer U } @@ -60,6 +72,16 @@ export interface CustomResponseOptions extends HttpResponseOptions { statusCode: number; } +/** + * HTTP response parameters + * @public + */ +export type RedirectResponseOptions = HttpResponseOptions & { + headers: { + location: string; + }; +}; + export const responseFactory = { // Success /** @@ -107,11 +129,12 @@ export const responseFactory = { /** * Redirect to a different URI. * Status code: `302`. - * @param url - an absolute or relative URI used to redirect the client to another resource. - * @param options - {@link HttpResponseOptions} configures HTTP response parameters. + * @param payload - payload to send to the client + * @param options - {@link RedirectResponseOptions} configures HTTP response parameters. + * Expects `location` header to be set. */ - redirected: (url: string, options: HttpResponseOptions = {}) => - new KibanaResponse(302, url, options), + redirected: (payload: T, options: RedirectResponseOptions) => + new KibanaResponse(302, payload, options), // Client error /** diff --git a/src/core/server/http/router/response_adapter.ts b/src/core/server/http/router/response_adapter.ts index 8009a09d237c87..30f031607818e5 100644 --- a/src/core/server/http/router/response_adapter.ts +++ b/src/core/server/http/router/response_adapter.ts @@ -16,13 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -import { ResponseObject, ResponseToolkit } from 'hapi'; +import { ResponseObject as HapiResponseObject, ResponseToolkit as HapiResponseToolkit } from 'hapi'; import typeDetect from 'type-detect'; import { HttpResponsePayload, KibanaResponse } from './response'; import { ResponseError } from './response_error'; -function setHeaders(response: ResponseObject, headers: Record = {}) { +function setHeaders(response: HapiResponseObject, headers: Record = {}) { Object.entries(headers).forEach(([header, value]) => { if (value !== undefined) { // Hapi typings for header accept only string, although string[] is a valid value @@ -39,7 +39,7 @@ const statusHelpers = { }; export class HapiResponseAdapter { - constructor(private readonly responseToolkit: ResponseToolkit) {} + constructor(private readonly responseToolkit: HapiResponseToolkit) {} public toBadRequest(message: string) { return this.responseToolkit.response({ error: message }).code(400); } @@ -81,12 +81,16 @@ export class HapiResponseAdapter { return this.responseToolkit.response(kibanaResponse.payload).code(kibanaResponse.status); } - private toRedirect(kibanaResponse: KibanaResponse) { - const url = kibanaResponse.payload; - if (typeof url !== 'string') { - throw new Error(`expected redirection url, but given ${typeDetect(url)}`); + private toRedirect(kibanaResponse: KibanaResponse) { + const { headers } = kibanaResponse.options; + if (!headers || typeof headers.location !== 'string') { + throw new Error("expected 'location' header to be set"); } - return this.responseToolkit.redirect(url).code(kibanaResponse.status); + + return this.responseToolkit + .response(kibanaResponse.payload) + .redirect(headers.location) + .code(kibanaResponse.status); } private toError(kibanaResponse: KibanaResponse) { diff --git a/src/core/server/http/router/response_error.ts b/src/core/server/http/router/response_error.ts index cd19a7a57b4601..81e5cce529ddbc 100644 --- a/src/core/server/http/router/response_error.ts +++ b/src/core/server/http/router/response_error.ts @@ -34,6 +34,8 @@ export interface ResponseErrorMeta { export class ResponseError extends Error { constructor(error: Error | string, public readonly meta?: ResponseErrorMeta) { super(typeof error === 'string' ? error : error.message); + // Set the prototype explicitly, see: + // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work Object.setPrototypeOf(this, ResponseError.prototype); } } diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 8ed95047aa91d8..6f8e95c0037456 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -135,7 +135,6 @@ export interface CoreSetup { registerOnPreAuth: HttpServiceSetup['registerOnPreAuth']; registerAuth: HttpServiceSetup['registerAuth']; registerOnPostAuth: HttpServiceSetup['registerOnPostAuth']; - registerRouter: HttpServiceSetup['registerRouter']; basePath: HttpServiceSetup['basePath']; createNewServer: HttpServiceSetup['createNewServer']; isTlsEnabled: HttpServiceSetup['isTlsEnabled']; diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 4ec489ee880cec..fcc8a26f51b4b0 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -122,7 +122,6 @@ export function createPluginSetupContext( registerOnPreAuth: deps.http.registerOnPreAuth, registerAuth: deps.http.registerAuth, registerOnPostAuth: deps.http.registerOnPostAuth, - registerRouter: deps.http.registerRouter, basePath: deps.http.basePath, createNewServer: deps.http.createNewServer, isTlsEnabled: deps.http.isTlsEnabled, From 3f6e3af2c85ff70a3a7993947131d8a3ac2b9bfe Mon Sep 17 00:00:00 2001 From: restrry Date: Mon, 29 Jul 2019 10:57:02 +0200 Subject: [PATCH 09/15] move tests under integration test cathegory --- .../response.test.ts => integration_tests/router.test.ts} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename src/core/server/http/{router/response.test.ts => integration_tests/router.test.ts} (99%) diff --git a/src/core/server/http/router/response.test.ts b/src/core/server/http/integration_tests/router.test.ts similarity index 99% rename from src/core/server/http/router/response.test.ts rename to src/core/server/http/integration_tests/router.test.ts index 1f09f37f55174e..89359a9de80c60 100644 --- a/src/core/server/http/router/response.test.ts +++ b/src/core/server/http/integration_tests/router.test.ts @@ -22,12 +22,12 @@ import Boom from 'boom'; import supertest from 'supertest'; import { ByteSizeValue, schema } from '@kbn/config-schema'; -import { HttpConfig, Router } from '..'; -import { createResponseError } from './response_error'; -import { loggingServiceMock } from '../../logging/logging_service.mock'; -import { LoggerFactory } from '../../logging/'; +import { createResponseError, HttpConfig, Router } from '..'; import { HttpServer } from '../http_server'; +import { LoggerFactory } from '../../logging'; +import { loggingServiceMock } from '../../logging/logging_service.mock'; + let server: HttpServer; let logger: LoggerFactory; From 6445845cd25f481940920748e548f60e0547999b Mon Sep 17 00:00:00 2001 From: restrry Date: Mon, 29 Jul 2019 10:57:16 +0200 Subject: [PATCH 10/15] update docs --- .../core/server/kibana-plugin-server.coresetup.http.md | 1 - docs/development/core/server/kibana-plugin-server.coresetup.md | 2 +- src/core/server/server.api.md | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.http.md b/docs/development/core/server/kibana-plugin-server.coresetup.http.md index 379c53f34d3d40..c9206b7a7e711c 100644 --- a/docs/development/core/server/kibana-plugin-server.coresetup.http.md +++ b/docs/development/core/server/kibana-plugin-server.coresetup.http.md @@ -12,7 +12,6 @@ http: { registerOnPreAuth: HttpServiceSetup['registerOnPreAuth']; registerAuth: HttpServiceSetup['registerAuth']; registerOnPostAuth: HttpServiceSetup['registerOnPostAuth']; - registerRouter: HttpServiceSetup['registerRouter']; basePath: HttpServiceSetup['basePath']; createNewServer: HttpServiceSetup['createNewServer']; isTlsEnabled: HttpServiceSetup['isTlsEnabled']; diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.md b/docs/development/core/server/kibana-plugin-server.coresetup.md index 2d34d4288bfe29..f4653d7f435798 100644 --- a/docs/development/core/server/kibana-plugin-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-server.coresetup.md @@ -17,5 +17,5 @@ export interface CoreSetup | Property | Type | Description | | --- | --- | --- | | [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | {
adminClient$: Observable<ClusterClient>;
dataClient$: Observable<ClusterClient>;
createClient: (type: string, clientConfig?: Partial<ElasticsearchClientConfig>) => ClusterClient;
} | | -| [http](./kibana-plugin-server.coresetup.http.md) | {
createCookieSessionStorageFactory: HttpServiceSetup['createCookieSessionStorageFactory'];
registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];
registerAuth: HttpServiceSetup['registerAuth'];
registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];
registerRouter: HttpServiceSetup['registerRouter'];
basePath: HttpServiceSetup['basePath'];
createNewServer: HttpServiceSetup['createNewServer'];
isTlsEnabled: HttpServiceSetup['isTlsEnabled'];
} | | +| [http](./kibana-plugin-server.coresetup.http.md) | {
createCookieSessionStorageFactory: HttpServiceSetup['createCookieSessionStorageFactory'];
registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];
registerAuth: HttpServiceSetup['registerAuth'];
registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];
basePath: HttpServiceSetup['basePath'];
createNewServer: HttpServiceSetup['createNewServer'];
isTlsEnabled: HttpServiceSetup['isTlsEnabled'];
} | | diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index ef2a1225c79840..c8ec7c12af4729 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -99,7 +99,6 @@ export interface CoreSetup { registerOnPreAuth: HttpServiceSetup['registerOnPreAuth']; registerAuth: HttpServiceSetup['registerAuth']; registerOnPostAuth: HttpServiceSetup['registerOnPostAuth']; - registerRouter: HttpServiceSetup['registerRouter']; basePath: HttpServiceSetup['basePath']; createNewServer: HttpServiceSetup['createNewServer']; isTlsEnabled: HttpServiceSetup['isTlsEnabled']; From 317af2f20e1ae53d04fd51a078f0922251229546 Mon Sep 17 00:00:00 2001 From: restrry Date: Tue, 30 Jul 2019 09:37:38 +0200 Subject: [PATCH 11/15] get rid of KibanResponseError class --- src/core/server/http/index.ts | 1 - .../http/integration_tests/router.test.ts | 171 ++++++++++++++++-- src/core/server/http/router/index.ts | 2 +- src/core/server/http/router/response.ts | 41 +++-- .../server/http/router/response_adapter.ts | 30 ++- .../server/http/router/response_error.test.ts | 39 ---- src/core/server/http/router/response_error.ts | 50 ----- src/core/server/index.ts | 1 - 8 files changed, 208 insertions(+), 127 deletions(-) delete mode 100644 src/core/server/http/router/response_error.test.ts delete mode 100644 src/core/server/http/router/response_error.ts diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index 95f45b7d9ad1a5..eff7de0979c119 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -21,7 +21,6 @@ export { config, HttpConfig, HttpConfigType } from './http_config'; export { HttpService, HttpServiceSetup, HttpServiceStart } from './http_service'; export { GetAuthHeaders } from './auth_headers_storage'; export { - createResponseError, isRealRequest, KibanaRequest, KibanaRequestRoute, diff --git a/src/core/server/http/integration_tests/router.test.ts b/src/core/server/http/integration_tests/router.test.ts index 89359a9de80c60..1d3ee4643830d9 100644 --- a/src/core/server/http/integration_tests/router.test.ts +++ b/src/core/server/http/integration_tests/router.test.ts @@ -22,7 +22,7 @@ import Boom from 'boom'; import supertest from 'supertest'; import { ByteSizeValue, schema } from '@kbn/config-schema'; -import { createResponseError, HttpConfig, Router } from '..'; +import { HttpConfig, Router } from '..'; import { HttpServer } from '../http_server'; import { LoggerFactory } from '../../logging'; @@ -562,12 +562,29 @@ describe('Response factory', () => { expect(result.body).toEqual({ error: 'some message' }); }); + it('400 Bad request with default message', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.badRequest(); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(400); + + expect(result.body).toEqual({ error: 'Bad Request' }); + }); + it('400 Bad request with additional data', async () => { const router = new Router('/'); router.get({ path: '/', validate: false }, (req, res) => { - const error = createResponseError('some message', { data: ['good', 'bad'] }); - return res.badRequest(error); + return res.badRequest({ message: 'some message', meta: { data: ['good', 'bad'] } }); }); const { registerRouter, server: innerServer } = await server.setup(config); @@ -610,6 +627,24 @@ describe('Response factory', () => { expect(result.header['www-authenticate']).toBe('challenge'); }); + it('401 Unauthorized with default message', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.unauthorized(); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(401); + + expect(result.body.error).toBe('Unauthorized'); + }); + it('403 Forbidden', async () => { const router = new Router('/'); @@ -629,6 +664,24 @@ describe('Response factory', () => { expect(result.body.error).toBe('reason'); }); + it('403 Forbidden with default message', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.forbidden(); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(403); + + expect(result.body.error).toBe('Forbidden'); + }); + it('404 Not Found', async () => { const router = new Router('/'); @@ -648,6 +701,24 @@ describe('Response factory', () => { expect(result.body.error).toBe('file is not found'); }); + it('404 Not Found with default message', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.notFound(); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(404); + + expect(result.body.error).toBe('Not Found'); + }); + it('409 Conflict', async () => { const router = new Router('/'); @@ -666,6 +737,24 @@ describe('Response factory', () => { expect(result.body.error).toBe('stale version'); }); + + it('409 Conflict with default message', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.conflict(); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(409); + + expect(result.body.error).toBe('Conflict'); + }); }); describe('Custom', () => { @@ -767,10 +856,44 @@ describe('Response factory', () => { const router = new Router('/'); router.get({ path: '/', validate: false }, (req, res) => { - const error = createResponseError('unauthorized', { errorCode: 'K401' }); - return res.custom(error, { - statusCode: 401, - }); + return res.custom( + { + message: 'unauthorized', + meta: { errorCode: 'K401' }, + }, + { + statusCode: 401, + } + ); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(401); + + expect(result.body).toEqual({ + error: 'unauthorized', + meta: { errorCode: 'K401' }, + }); + }); + + it('creates error response with additional data and error object', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.custom( + { + message: new Error('unauthorized'), + meta: { errorCode: 'K401' }, + }, + { + statusCode: 401, + } + ); }); const { registerRouter, server: innerServer } = await server.setup(config); @@ -812,8 +935,7 @@ describe('Response factory', () => { const router = new Router('/'); router.get({ path: '/', validate: false }, (req, res) => { - const error = createResponseError('reason'); - return res.custom(error, { + return res.custom('reason', { statusCode: 500, }); }); @@ -835,7 +957,7 @@ describe('Response factory', () => { router.get({ path: '/', validate: false }, (req, res) => { return res.custom( - { message: 'error-message' }, + { error: 'error-message' }, { statusCode: 401, } @@ -854,7 +976,34 @@ describe('Response factory', () => { expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` Array [ Array [ - [Error: expected Error object, but given Object], + [Error: expected error message to be provided], + ], + ] + `); + }); + + it('throws if an error not provided', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.custom(undefined, { + statusCode: 401, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body.error).toBe('An internal server error occurred.'); + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: expected error message to be provided], ], ] `); diff --git a/src/core/server/http/router/index.ts b/src/core/server/http/router/index.ts index 037a1fc1fdd5ef..eefa74cee0802e 100644 --- a/src/core/server/http/router/index.ts +++ b/src/core/server/http/router/index.ts @@ -21,4 +21,4 @@ export { Headers, filterHeaders, ResponseHeaders } from './headers'; export { Router } from './router'; export { KibanaRequest, KibanaRequestRoute, ensureRawRequest, isRealRequest } from './request'; export { RouteMethod, RouteConfigOptions } from './route'; -export { createResponseError, ResponseError, ResponseErrorMeta } from './response_error'; +export { ResponseError, ResponseErrorMeta } from './response'; diff --git a/src/core/server/http/router/response.ts b/src/core/server/http/router/response.ts index 930674c9cdc195..9aa01f75b2f8d8 100644 --- a/src/core/server/http/router/response.ts +++ b/src/core/server/http/router/response.ts @@ -19,7 +19,23 @@ import { IncomingHttpHeaders } from 'http'; import { Stream } from 'stream'; -import { ResponseError } from './response_error'; +/** + * Additional metadata to enhance error output or provide error details. + * @public + */ +export interface ResponseErrorMeta { + data?: Record; + errorCode?: string; + docLink?: string; +} + +export type ResponseError = + | string + | Error + | { + message: string | Error; + meta?: ResponseErrorMeta; + }; export class KibanaResponse { constructor( @@ -90,7 +106,7 @@ export const responseFactory = { * @param payload - {@link HttpResponsePayload} payload to send to the client * @param options - {@link HttpResponseOptions} configures HTTP response parameters. */ - ok: (payload: T, options: HttpResponseOptions = {}) => + ok: (payload: HttpResponsePayload, options: HttpResponseOptions = {}) => new KibanaResponse(200, payload, options), /** @@ -99,7 +115,7 @@ export const responseFactory = { * @param payload - {@link HttpResponsePayload} payload to send to the client * @param options - {@link HttpResponseOptions} configures HTTP response parameters. */ - accepted: (payload?: T, options: HttpResponseOptions = {}) => + accepted: (payload?: HttpResponsePayload, options: HttpResponseOptions = {}) => new KibanaResponse(202, payload, options), /** @@ -114,10 +130,7 @@ export const responseFactory = { * @param payload - {@link HttpResponsePayload} payload to send to the client * @param options - {@link CustomResponseOptions} configures HTTP response parameters. */ - custom: ( - payload: T, - options: CustomResponseOptions - ) => { + custom: (payload: HttpResponsePayload | ResponseError, options: CustomResponseOptions) => { if (!options || !options.statusCode) { throw new Error(`options.statusCode is expected to be set. given options: ${options}`); } @@ -133,7 +146,7 @@ export const responseFactory = { * @param options - {@link RedirectResponseOptions} configures HTTP response parameters. * Expects `location` header to be set. */ - redirected: (payload: T, options: RedirectResponseOptions) => + redirected: (payload: HttpResponsePayload, options: RedirectResponseOptions) => new KibanaResponse(302, payload, options), // Client error @@ -143,7 +156,7 @@ export const responseFactory = { * @param error - {@link ResponseError} Error object containing message and other error details to pass to the client * @param options - {@link HttpResponseOptions} configures HTTP response parameters. */ - badRequest: (error: T, options: HttpResponseOptions = {}) => + badRequest: (error: ResponseError = 'Bad Request', options: HttpResponseOptions = {}) => new KibanaResponse(400, error, options), /** @@ -152,7 +165,7 @@ export const responseFactory = { * @param error - {@link ResponseError} Error object containing message and other error details to pass to the client * @param options - {@link HttpResponseOptions} configures HTTP response parameters. */ - unauthorized: (error: T, options: HttpResponseOptions = {}) => + unauthorized: (error: ResponseError = 'Unauthorized', options: HttpResponseOptions = {}) => new KibanaResponse(401, error, options), /** @@ -161,7 +174,7 @@ export const responseFactory = { * @param error - {@link ResponseError} Error object containing message and other error details to pass to the client * @param options - {@link HttpResponseOptions} configures HTTP response parameters. */ - forbidden: (error: T, options: HttpResponseOptions = {}) => + forbidden: (error: ResponseError = 'Forbidden', options: HttpResponseOptions = {}) => new KibanaResponse(403, error, options), /** @@ -170,7 +183,7 @@ export const responseFactory = { * @param error - {@link ResponseError} Error object containing message and other error details to pass to the client * @param options - {@link HttpResponseOptions} configures HTTP response parameters. */ - notFound: (error: T, options: HttpResponseOptions = {}) => + notFound: (error: ResponseError = 'Not Found', options: HttpResponseOptions = {}) => new KibanaResponse(404, error, options), /** @@ -179,7 +192,7 @@ export const responseFactory = { * @param error - {@link ResponseError} Error object containing message and other error details to pass to the client * @param options - {@link HttpResponseOptions} configures HTTP response parameters. */ - conflict: (error: T, options: HttpResponseOptions = {}) => + conflict: (error: ResponseError = 'Conflict', options: HttpResponseOptions = {}) => new KibanaResponse(409, error, options), // Server error @@ -189,7 +202,7 @@ export const responseFactory = { * @param error - {@link ResponseError} Error object containing message and other error details to pass to the client * @param options - {@link HttpResponseOptions} configures HTTP response parameters. */ - internal: (error: T, options: HttpResponseOptions = {}) => + internal: (error: ResponseError = 'Internal Error', options: HttpResponseOptions = {}) => new KibanaResponse(500, error, options), }; diff --git a/src/core/server/http/router/response_adapter.ts b/src/core/server/http/router/response_adapter.ts index 30f031607818e5..d8208b4a34058d 100644 --- a/src/core/server/http/router/response_adapter.ts +++ b/src/core/server/http/router/response_adapter.ts @@ -19,8 +19,7 @@ import { ResponseObject as HapiResponseObject, ResponseToolkit as HapiResponseToolkit } from 'hapi'; import typeDetect from 'type-detect'; -import { HttpResponsePayload, KibanaResponse } from './response'; -import { ResponseError } from './response_error'; +import { HttpResponsePayload, KibanaResponse, ResponseError } from './response'; function setHeaders(response: HapiResponseObject, headers: Record = {}) { Object.entries(headers).forEach(([header, value]) => { @@ -94,13 +93,24 @@ export class HapiResponseAdapter { } private toError(kibanaResponse: KibanaResponse) { - if (!(kibanaResponse.payload instanceof Error)) { - throw new Error(`expected Error object, but given ${typeDetect(kibanaResponse.payload)}`); - } - const payload = { - error: kibanaResponse.payload.message, - meta: kibanaResponse.payload.meta, - }; - return this.responseToolkit.response(payload).code(kibanaResponse.status); + const { payload } = kibanaResponse; + return this.responseToolkit + .response({ + error: getErrorMessage(payload), + meta: getErrorMeta(payload), + }) + .code(kibanaResponse.status); + } +} + +function getErrorMessage(payload?: ResponseError): string { + if (!payload) { + throw new Error('expected error message to be provided'); } + if (typeof payload === 'string') return payload; + return getErrorMessage(payload.message); +} + +function getErrorMeta(payload?: ResponseError) { + return typeof payload === 'object' && 'meta' in payload ? payload.meta : undefined; } diff --git a/src/core/server/http/router/response_error.test.ts b/src/core/server/http/router/response_error.test.ts deleted file mode 100644 index 5a29077f2c182e..00000000000000 --- a/src/core/server/http/router/response_error.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { createResponseError, ResponseError } from './response_error'; - -describe('createResponseError', () => { - it('creates ResponseError instance', async () => { - expect(createResponseError(new Error('reason'))).toBeInstanceOf(ResponseError); - }); - - it('inherits from Error', async () => { - expect(createResponseError(new Error('reason'))).toBeInstanceOf(Error); - }); - - it('stores additional data', async () => { - const error = createResponseError(new Error('reason'), { data: { key: 'value' } }); - expect(error.meta).toEqual({ data: { key: 'value' } }); - }); - - it('accept a string as error message', async () => { - expect(createResponseError('reason')).toBeInstanceOf(Error); - expect(createResponseError('reason').message).toBe('reason'); - }); -}); diff --git a/src/core/server/http/router/response_error.ts b/src/core/server/http/router/response_error.ts deleted file mode 100644 index 81e5cce529ddbc..00000000000000 --- a/src/core/server/http/router/response_error.ts +++ /dev/null @@ -1,50 +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. - */ - -/** - * Additional meta-data to enhance error output or provide error details. - * @public - */ -export interface ResponseErrorMeta { - data?: Record; - errorCode?: string; - docLink?: string; -} - -/** - * Kibana ResponseError object to store error details - * @public - */ -export class ResponseError extends Error { - constructor(error: Error | string, public readonly meta?: ResponseErrorMeta) { - super(typeof error === 'string' ? error : error.message); - // Set the prototype explicitly, see: - // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work - Object.setPrototypeOf(this, ResponseError.prototype); - } -} - -/** - * Creates Kibana ResponseError instance - * @param error - Error object or message to wrap - * @param meta - additional meta-data to pass to the client - * @public - */ -export const createResponseError = (error: Error | string, meta?: ResponseErrorMeta) => - new ResponseError(error, meta); diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 6f8e95c0037456..2bbcaa58f6af52 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -64,7 +64,6 @@ export { AuthResultParams, AuthToolkit, GetAuthHeaders, - createResponseError, KibanaRequest, KibanaRequestRoute, OnPreAuthHandler, From 179ab37bf0b2f5ef3c5a0efa91fedee8853c8426 Mon Sep 17 00:00:00 2001 From: restrry Date: Tue, 30 Jul 2019 10:59:08 +0200 Subject: [PATCH 12/15] update docs --- ...ibana-plugin-server.createresponseerror.md | 13 ----------- .../core/server/kibana-plugin-server.md | 10 ++------- ...ugin-server.responseerror.(constructor).md | 21 ------------------ .../kibana-plugin-server.responseerror.md | 22 +++++-------------- ...kibana-plugin-server.responseerror.meta.md | 11 ---------- .../kibana-plugin-server.responseerrormeta.md | 2 +- src/core/server/http/router/response.ts | 5 ++++- src/core/server/server.api.md | 12 ++++------ 8 files changed, 17 insertions(+), 79 deletions(-) delete mode 100644 docs/development/core/server/kibana-plugin-server.createresponseerror.md delete mode 100644 docs/development/core/server/kibana-plugin-server.responseerror.(constructor).md delete mode 100644 docs/development/core/server/kibana-plugin-server.responseerror.meta.md diff --git a/docs/development/core/server/kibana-plugin-server.createresponseerror.md b/docs/development/core/server/kibana-plugin-server.createresponseerror.md deleted file mode 100644 index 65dcda022ecae2..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.createresponseerror.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [createResponseError](./kibana-plugin-server.createresponseerror.md) - -## createResponseError variable - -Creates Kibana ResponseError instance - -Signature: - -```typescript -createResponseError: (error: string | Error, meta?: ResponseErrorMeta | undefined) => ResponseError -``` diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 862b2567737bb5..bd4ef847230575 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -17,7 +17,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ClusterClient](./kibana-plugin-server.clusterclient.md) | Represents an Elasticsearch cluster API client and allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)). | | [ElasticsearchErrorHelpers](./kibana-plugin-server.elasticsearcherrorhelpers.md) | Helpers for working with errors returned from the Elasticsearch service.Since the internal data of errors are subject to change, consumers of the Elasticsearch service should always use these helpers to classify errors instead of checking error internals such as body.error.header[WWW-Authenticate] | | [KibanaRequest](./kibana-plugin-server.kibanarequest.md) | Kibana specific abstraction for an incoming request. | -| [ResponseError](./kibana-plugin-server.responseerror.md) | Kibana ResponseError object to store error details | | [Router](./kibana-plugin-server.router.md) | | | [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | | | [SavedObjectsSchema](./kibana-plugin-server.savedobjectsschema.md) | | @@ -50,7 +49,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. | | [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) | | | [PluginsServiceStart](./kibana-plugin-server.pluginsservicestart.md) | | -| [ResponseErrorMeta](./kibana-plugin-server.responseerrormeta.md) | Additional meta-data to enhance error output or provide error details. | +| [ResponseErrorMeta](./kibana-plugin-server.responseerrormeta.md) | Additional metadata to enhance error output or provide error details. | | [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md) | Route specific configuration. | | [SavedObject](./kibana-plugin-server.savedobject.md) | | | [SavedObjectAttributes](./kibana-plugin-server.savedobjectattributes.md) | | @@ -71,12 +70,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SessionStorage](./kibana-plugin-server.sessionstorage.md) | Provides an interface to store and retrieve data across requests. | | [SessionStorageFactory](./kibana-plugin-server.sessionstoragefactory.md) | SessionStorage factory to bind one to an incoming request | -## Variables - -| Variable | Description | -| --- | --- | -| [createResponseError](./kibana-plugin-server.createresponseerror.md) | Creates Kibana ResponseError instance | - ## Type Aliases | Type Alias | Description | @@ -93,6 +86,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginInitializer](./kibana-plugin-server.plugininitializer.md) | The plugin export at the root of a plugin's server directory should conform to this interface. | | [PluginName](./kibana-plugin-server.pluginname.md) | Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays that use it as a key or value more obvious. | | [RecursiveReadonly](./kibana-plugin-server.recursivereadonly.md) | | +| [ResponseError](./kibana-plugin-server.responseerror.md) | Error message and optional data send to the client in case of error. | | [RouteMethod](./kibana-plugin-server.routemethod.md) | The set of common HTTP methods supported by Kibana routing. | | [SavedObjectsClientContract](./kibana-plugin-server.savedobjectsclientcontract.md) | \#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError() helpers exposed at SavedObjectsErrorHelpers should be used to understand and manage error responses from the SavedObjectsClient.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type or doing substring checks on error.body.error.reason, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.\#\#\# 503s from missing indexUnlike all other methods, create requests are supposed to succeed even when the Kibana index does not exist because it will be automatically created by elasticsearch. When that is not the case it is because Elasticsearch's action.auto_create_index setting prevents it from being created automatically so we throw a special 503 with the intention of informing the user that their Elasticsearch settings need to be updated.See [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | | [SavedObjectsClientWrapperFactory](./kibana-plugin-server.savedobjectsclientwrapperfactory.md) | | diff --git a/docs/development/core/server/kibana-plugin-server.responseerror.(constructor).md b/docs/development/core/server/kibana-plugin-server.responseerror.(constructor).md deleted file mode 100644 index d148bfca478715..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.responseerror.(constructor).md +++ /dev/null @@ -1,21 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ResponseError](./kibana-plugin-server.responseerror.md) > [(constructor)](./kibana-plugin-server.responseerror.(constructor).md) - -## ResponseError.(constructor) - -Constructs a new instance of the `ResponseError` class - -Signature: - -```typescript -constructor(error: Error | string, meta?: ResponseErrorMeta | undefined); -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| error | Error | string | | -| meta | ResponseErrorMeta | undefined | | - diff --git a/docs/development/core/server/kibana-plugin-server.responseerror.md b/docs/development/core/server/kibana-plugin-server.responseerror.md index 9182594f5366c0..6aa4a4e97ff72e 100644 --- a/docs/development/core/server/kibana-plugin-server.responseerror.md +++ b/docs/development/core/server/kibana-plugin-server.responseerror.md @@ -2,25 +2,15 @@ [Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ResponseError](./kibana-plugin-server.responseerror.md) -## ResponseError class +## ResponseError type -Kibana ResponseError object to store error details +Error message and optional data send to the client in case of error. Signature: ```typescript -export declare class ResponseError extends Error +export declare type ResponseError = string | Error | { + message: string | Error; + meta?: ResponseErrorMeta; +}; ``` - -## Constructors - -| Constructor | Modifiers | Description | -| --- | --- | --- | -| [(constructor)(error, meta)](./kibana-plugin-server.responseerror.(constructor).md) | | Constructs a new instance of the ResponseError class | - -## Properties - -| Property | Modifiers | Type | Description | -| --- | --- | --- | --- | -| [meta](./kibana-plugin-server.responseerror.meta.md) | | ResponseErrorMeta | undefined | | - diff --git a/docs/development/core/server/kibana-plugin-server.responseerror.meta.md b/docs/development/core/server/kibana-plugin-server.responseerror.meta.md deleted file mode 100644 index 0c020730c00b5a..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.responseerror.meta.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ResponseError](./kibana-plugin-server.responseerror.md) > [meta](./kibana-plugin-server.responseerror.meta.md) - -## ResponseError.meta property - -Signature: - -```typescript -readonly meta?: ResponseErrorMeta | undefined; -``` diff --git a/docs/development/core/server/kibana-plugin-server.responseerrormeta.md b/docs/development/core/server/kibana-plugin-server.responseerrormeta.md index 81965704dd1766..9ab351d013dd7b 100644 --- a/docs/development/core/server/kibana-plugin-server.responseerrormeta.md +++ b/docs/development/core/server/kibana-plugin-server.responseerrormeta.md @@ -4,7 +4,7 @@ ## ResponseErrorMeta interface -Additional meta-data to enhance error output or provide error details. +Additional metadata to enhance error output or provide error details. Signature: diff --git a/src/core/server/http/router/response.ts b/src/core/server/http/router/response.ts index 9aa01f75b2f8d8..65db87f8ae8f8e 100644 --- a/src/core/server/http/router/response.ts +++ b/src/core/server/http/router/response.ts @@ -28,7 +28,10 @@ export interface ResponseErrorMeta { errorCode?: string; docLink?: string; } - +/** + * Error message and optional data send to the client in case of error. + * @public + */ export type ResponseError = | string | Error diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index c8ec7c12af4729..6ab535a66cb360 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -109,9 +109,6 @@ export interface CoreSetup { export interface CoreStart { } -// @public -export const createResponseError: (error: string | Error, meta?: ResponseErrorMeta | undefined) => ResponseError; - // @public export interface DiscoveredPlugin { readonly configPath: ConfigPath; @@ -391,11 +388,10 @@ export type RecursiveReadonly = T extends (...args: any[]) => any ? T : T ext }> : T; // @public -export class ResponseError extends Error { - constructor(error: Error | string, meta?: ResponseErrorMeta | undefined); - // (undocumented) - readonly meta?: ResponseErrorMeta | undefined; -} +export type ResponseError = string | Error | { + message: string | Error; + meta?: ResponseErrorMeta; +}; // @public export interface ResponseErrorMeta { From 17285223e8ac89af41133d4fbe37b3affc53bba9 Mon Sep 17 00:00:00 2001 From: restrry Date: Tue, 30 Jul 2019 14:09:11 +0200 Subject: [PATCH 13/15] add TSDoc --- src/core/MIGRATION.md | 158 ++++++++++++++++++ .../server/elasticsearch/cluster_client.ts | 5 +- src/core/server/elasticsearch/index.ts | 2 +- src/core/server/http/auth_headers_storage.ts | 12 +- src/core/server/http/auth_state_storage.ts | 41 ++++- src/core/server/http/base_path_service.ts | 9 +- .../server/http/cookie_session_storage.ts | 16 ++ src/core/server/http/http_server.ts | 44 +++-- src/core/server/http/index.ts | 13 ++ src/core/server/http/router/headers.ts | 43 ++++- src/core/server/http/router/index.ts | 25 ++- src/core/server/http/router/request.ts | 20 ++- src/core/server/http/router/response.ts | 45 ++--- src/core/server/http/router/route.ts | 43 ++++- src/core/server/http/router/router.ts | 72 +++++++- src/core/server/index.ts | 18 +- 16 files changed, 477 insertions(+), 89 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 295602b6e14866..400890da3fe764 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -1115,6 +1115,164 @@ class MyPlugin { } ``` +### Handle HTTP request with New Platform HTTP Service +Kibana HTTP Service provides own abstraction for work with HTTP stack. +Plugins don't have direct access to `hapi` server and its primitives anymore. Moreover, +you shouldn't rely on the fact that HTTP Service uses one or another library under the hood, there are high chances that we can switch to another stack at all. If HTTP Service +lacks needed functionality we happy to discuss and support your needs. + +To handle an incoming request in your plugin you should: +- create Router instance. Use `plugin id` as a prefix path segment for your routes. +```ts +import { Router } from 'src/core/server'; +const router = new Router('my-app'); +``` + +- use `@kbn/config-schema` package to create a schema to validate `request params`, `query`, `body` if an incoming request used them to pass additional details. Every incoming request will be validated against the created schema. If validation failed, the request is rejected with `400` status and `Bad request` error. +To opt out of validating the request, specify `false`. +```ts +import { schema, TypeOf } from '@kbn/config-schema'; +const validate = { + params: schema.object({ + id: schema.string(), + }), +}; +``` + +- declare a function to respond to incoming request. +The function will receive `request` object containing request details: url, headers, matched route, as well as validated `params`, `query`, `body`. +And `createResponse` object instructing HTTP server to create HTTP response with information sent back to the client as the response body, headers, and HTTP status. +Unlike, `hapi` route handler in the Legacy platform, any exception raised during the handler call will generate `500 Server error` response and log error details for further investigation. +```ts + const handler = async (request: KibanaRequest, createResponse: ResponseFactory) => { + const data = await findObject(request.params.id); + // creates a command to respond with 'not found' error + if (!data) return createResponse.notFound(); + // creates a command to send found data to the client and set response headers + return createResponse.ok(data, { + headers: { + 'content-type': 'application/json' + } + }); + } +``` + +- register route handler for GET request to 'my-app/path/{id}' path +```ts +import { schema, TypeOf } from '@kbn/config-schema'; +import { Router } from 'src/core/server'; +const router = new Router('my-app'); + +const validate = { + params: schema.object({ + id: schema.string(), + }), +}; + +router.get( + { + path: 'path/{id}', + validate + }, + async (request, createResponse) => { + const data = await findObject(request.params.id); + if (!data) return createResponse.notFound(); + return createResponse.ok(data, { + headers: { + 'content-type': 'application/json' + } + }); + } +); +``` +#### What data I can send back with a response? +You have several options to create a response utilizing `createResponse`: +1. create a successful response. Supported types of response body are: +- `undefined`, no content to send. +- `string`, send text +- `JSON`, send JSON object, HTTP server will throw if given object is not valid (has circular references, for example) +- `Stream` send data stream +- `Buffer` send binary stream +```js +return response.ok(undefined); +return response.ok('ack'); +return response.ok({ id: '1' }); +return response.ok(Buffer.from(...);); + +const stream = new Stream.PassThrough(); +fs.createReadStream('./file').pipe(stream); +return res.ok(stream); +``` +HTTP headers are configurable via response factory parameter `options`. + +```js +return response.ok({ id: '1' }, { + headers: { + 'content-type': 'application/json' + } +}); +``` +2. create redirect response. Redirection URL is configures via 'Location' header. +```js +return response.redirected('The document has moved', { + headers: { + location: '/new-url', + }, +}); +``` +3. create error response. You may pass an error message to the client, where error message can be: +- `string` send message text +- `Error` send the message text of given Error object. +- `{ message: string | Error, meta: {data: Record, ...} }` - send message text and attach additional error metadata. +```js +return response.unauthorized('User has no access to the requested resource.', { + headers: { + 'WWW-Authenticate': 'challenge', + } +}) +return response.badRequest(); +return response.badRequest('validation error'); + +try { + // ... +} catch(error){ + return response.badRequest(error); +} + +return response.badRequest({ + message: 'validation error', + meta: { + data: { + requestBody: request.body, + failedFields: validationResult + }, + } +}); + +try { + // ... +} catch(error){ + return response.badRequest({ + message: error, + meta: { + data: { + requestBody: request.body, + }, + } +}); +} + +``` +4. create a custom response. It might happen that `response factory` doesn't cover your use case and you want to specify HTTP response status code as well. +```js +return response.custom('ok', { + statusCode: 201, + headers: { + location: '/created-url' + } +}) +``` + ### Mock core services in tests Core services already provide mocks to simplify testing and make sure plugins always rely on valid public contracts. ```typescript diff --git a/src/core/server/elasticsearch/cluster_client.ts b/src/core/server/elasticsearch/cluster_client.ts index c8500499d97029..aa40010bf3b1ac 100644 --- a/src/core/server/elasticsearch/cluster_client.ts +++ b/src/core/server/elasticsearch/cluster_client.ts @@ -18,10 +18,9 @@ */ import { Client } from 'elasticsearch'; import { get } from 'lodash'; -import { Request } from 'hapi'; import { ElasticsearchErrorHelpers } from './errors'; -import { GetAuthHeaders, isRealRequest } from '../http'; +import { GetAuthHeaders, isRealRequest, LegacyRequest } from '../http'; import { filterHeaders, Headers, KibanaRequest, ensureRawRequest } from '../http/router'; import { Logger } from '../logging'; import { @@ -36,8 +35,6 @@ import { ScopedClusterClient } from './scoped_cluster_client'; * @public */ -export type LegacyRequest = Request; - const noop = () => undefined; /** * The set of options that defines how API call should be made and result be diff --git a/src/core/server/elasticsearch/index.ts b/src/core/server/elasticsearch/index.ts index 1d439dfba49e9e..f732f9e39b9e30 100644 --- a/src/core/server/elasticsearch/index.ts +++ b/src/core/server/elasticsearch/index.ts @@ -18,7 +18,7 @@ */ export { ElasticsearchServiceSetup, ElasticsearchService } from './elasticsearch_service'; -export { CallAPIOptions, ClusterClient, FakeRequest, LegacyRequest } from './cluster_client'; +export { CallAPIOptions, ClusterClient, FakeRequest } from './cluster_client'; export { ScopedClusterClient, Headers, APICaller } from './scoped_cluster_client'; export { ElasticsearchClientConfig } from './elasticsearch_client_config'; export { config } from './elasticsearch_config'; diff --git a/src/core/server/http/auth_headers_storage.ts b/src/core/server/http/auth_headers_storage.ts index bc3b55b3718c01..469e194a61fed2 100644 --- a/src/core/server/http/auth_headers_storage.ts +++ b/src/core/server/http/auth_headers_storage.ts @@ -16,19 +16,21 @@ * specific language governing permissions and limitations * under the License. */ -import { Request } from 'hapi'; -import { KibanaRequest, ensureRawRequest } from './router'; +import { KibanaRequest, ensureRawRequest, LegacyRequest } from './router'; import { AuthHeaders } from './lifecycle/auth'; /** * Get headers to authenticate a user against Elasticsearch. + * @param request {@link KibanaRequest} - an incoming request. + * @return authentication headers {@link AuthHeaders} for - an incoming request. * @public * */ -export type GetAuthHeaders = (request: KibanaRequest | Request) => AuthHeaders | undefined; +export type GetAuthHeaders = (request: KibanaRequest | LegacyRequest) => AuthHeaders | undefined; +/** @internal */ export class AuthHeadersStorage { - private authHeadersCache = new WeakMap(); - public set = (request: KibanaRequest | Request, headers: AuthHeaders) => { + private authHeadersCache = new WeakMap(); + public set = (request: KibanaRequest | LegacyRequest, headers: AuthHeaders) => { this.authHeadersCache.set(ensureRawRequest(request), headers); }; public get: GetAuthHeaders = request => { diff --git a/src/core/server/http/auth_state_storage.ts b/src/core/server/http/auth_state_storage.ts index 79fd9ed64f3b54..059dc7f3803514 100644 --- a/src/core/server/http/auth_state_storage.ts +++ b/src/core/server/http/auth_state_storage.ts @@ -16,22 +16,51 @@ * specific language governing permissions and limitations * under the License. */ -import { Request } from 'hapi'; -import { KibanaRequest, ensureRawRequest } from './router'; +import { ensureRawRequest, KibanaRequest, LegacyRequest } from './router'; +/** + * Status indicating an outcome of the authentication. + * @public + */ export enum AuthStatus { + /** + * `auth` interceptor successfully authenticated a user + */ authenticated = 'authenticated', + /** + * `auth` interceptor failed user authentication + */ unauthenticated = 'unauthenticated', + /** + * `auth` interceptor has not been registered + */ unknown = 'unknown', } +/** + * Get authentication state for a request. Returned by `auth` interceptor. + * @param request {@link KibanaRequest} - an incoming request. + * @public + */ +export type GetAuthState = ( + request: KibanaRequest | LegacyRequest +) => { status: AuthStatus; state: unknown }; + +/** + * Return authentication status for a request. + * @param request {@link KibanaRequest} - an incoming request. + * @public + */ +export type IsAuthenticated = (request: KibanaRequest | LegacyRequest) => boolean; + +/** @internal */ export class AuthStateStorage { - private readonly storage = new WeakMap(); + private readonly storage = new WeakMap(); constructor(private readonly canBeAuthenticated: () => boolean) {} - public set = (request: KibanaRequest | Request, state: unknown) => { + public set = (request: KibanaRequest | LegacyRequest, state: unknown) => { this.storage.set(ensureRawRequest(request), state); }; - public get = (request: KibanaRequest | Request) => { + public get: GetAuthState = request => { const key = ensureRawRequest(request); const state = this.storage.get(key); const status: AuthStatus = this.storage.has(key) @@ -42,7 +71,7 @@ export class AuthStateStorage { return { status, state }; }; - public isAuthenticated = (request: KibanaRequest | Request) => { + public isAuthenticated: IsAuthenticated = request => { return this.get(request).status === AuthStatus.authenticated; }; } diff --git a/src/core/server/http/base_path_service.ts b/src/core/server/http/base_path_service.ts index df189d29f2f594..951463a2c9919f 100644 --- a/src/core/server/http/base_path_service.ts +++ b/src/core/server/http/base_path_service.ts @@ -16,24 +16,23 @@ * specific language governing permissions and limitations * under the License. */ -import { Request } from 'hapi'; -import { KibanaRequest, ensureRawRequest } from './router'; +import { ensureRawRequest, KibanaRequest, LegacyRequest } from './router'; import { modifyUrl } from '../../utils'; export class BasePath { - private readonly basePathCache = new WeakMap(); + private readonly basePathCache = new WeakMap(); constructor(private readonly serverBasePath?: string) {} - public get = (request: KibanaRequest | Request) => { + public get = (request: KibanaRequest | LegacyRequest) => { const requestScopePath = this.basePathCache.get(ensureRawRequest(request)) || ''; const serverBasePath = this.serverBasePath || ''; return `${serverBasePath}${requestScopePath}`; }; // should work only for KibanaRequest as soon as spaces migrate to NP - public set = (request: KibanaRequest | Request, requestSpecificBasePath: string) => { + public set = (request: KibanaRequest | LegacyRequest, requestSpecificBasePath: string) => { const rawRequest = ensureRawRequest(request); if (this.basePathCache.has(rawRequest)) { diff --git a/src/core/server/http/cookie_session_storage.ts b/src/core/server/http/cookie_session_storage.ts index 7b2569a1c6dd33..8a1b56d87fb4c9 100644 --- a/src/core/server/http/cookie_session_storage.ts +++ b/src/core/server/http/cookie_session_storage.ts @@ -24,10 +24,26 @@ import { KibanaRequest, ensureRawRequest } from './router'; import { SessionStorageFactory, SessionStorage } from './session_storage'; import { Logger } from '..'; +/** + * Configuration used to create HTTP session storage based on top of cookie mechanism. + * @public + */ export interface SessionStorageCookieOptions { + /** + * Name of the session cookie. + */ name: string; + /** + * A key used to encrypt a cookie value. Should be at least 32 characters long. + */ encryptionKey: string; + /** + * Function called to validate a cookie content. + */ validate: (sessionValue: T) => boolean | Promise; + /** + * Flag indicating whether the cookie should be sent only via a secure connection. + */ isSecure: boolean; } diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index c8b4c5c17f05e1..45cadb7d3811e4 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -25,21 +25,27 @@ import { createServer, getListenerOptions, getServerOptions } from './http_tools import { adoptToHapiAuthFormat, AuthenticationHandler } from './lifecycle/auth'; import { adoptToHapiOnPostAuthFormat, OnPostAuthHandler } from './lifecycle/on_post_auth'; import { adoptToHapiOnPreAuthFormat, OnPreAuthHandler } from './lifecycle/on_pre_auth'; -import { Router, KibanaRequest, ResponseHeaders } from './router'; +import { KibanaRequest, LegacyRequest, ResponseHeaders, Router } from './router'; import { SessionStorageCookieOptions, createCookieSessionStorageFactory, } from './cookie_session_storage'; import { SessionStorageFactory } from './session_storage'; -import { AuthStateStorage } from './auth_state_storage'; -import { AuthHeadersStorage } from './auth_headers_storage'; +import { AuthStateStorage, GetAuthState, IsAuthenticated } from './auth_state_storage'; +import { AuthHeadersStorage, GetAuthHeaders } from './auth_headers_storage'; import { BasePath } from './base_path_service'; +/** @public */ export interface HttpServerSetup { server: Server; + /** + * Add all the routes registered with `router` to HTTP server request listeners. + * @param router {@link Router} - a router with registered route handlers. + */ registerRouter: (router: Router) => void; /** * Creates cookie based session storage factory {@link SessionStorageFactory} + * @param cookieOptions {@link SessionStorageCookieOptions} - options to configure created cookie session storage. */ createCookieSessionStorageFactory: ( cookieOptions: SessionStorageCookieOptions @@ -49,35 +55,53 @@ export interface HttpServerSetup { * A handler should return a state to associate with the incoming request. * The state can be retrieved later via http.auth.get(..) * Only one AuthenticationHandler can be registered. + * @param handler {@link AuthenticationHandler} - function to perform authentication. */ registerAuth: (handler: AuthenticationHandler) => void; /** * To define custom logic to perform for incoming requests. Runs the handler before Auth - * hook performs a check that user has access to requested resources, so it's the only + * interceptor performs a check that user has access to requested resources, so it's the only * place when you can forward a request to another URL right on the server. * Can register any number of registerOnPostAuth, which are called in sequence * (from the first registered to the last). + * @param handler {@link OnPreAuthHandler} - function to call. */ registerOnPreAuth: (handler: OnPreAuthHandler) => void; /** - * To define custom logic to perform for incoming requests. Runs the handler after Auth hook + * To define custom logic to perform for incoming requests. Runs the handler after Auth interceptor * did make sure a user has access to the requested resource. * The auth state is available at stage via http.auth.get(..) * Can register any number of registerOnPreAuth, which are called in sequence * (from the first registered to the last). + * @param handler {@link OnPostAuthHandler} - function to call. */ registerOnPostAuth: (handler: OnPostAuthHandler) => void; basePath: { - get: (request: KibanaRequest | Request) => string; - set: (request: KibanaRequest | Request, basePath: string) => void; + /** + * returns `basePath` value, specific for an incoming request. + */ + get: (request: KibanaRequest | LegacyRequest) => string; + /** + * sets `basePath` value, specific for an incoming request. + */ + set: (request: KibanaRequest | LegacyRequest, basePath: string) => void; + /** + * returns a new `basePath` value, prefixed with passed `url`. + */ prepend: (url: string) => string; + /** + * returns a new `basePath` value, cleaned up from passed `url`. + */ remove: (url: string) => string; }; auth: { - get: AuthStateStorage['get']; - isAuthenticated: AuthStateStorage['isAuthenticated']; - getAuthHeaders: AuthHeadersStorage['get']; + get: GetAuthState; + isAuthenticated: IsAuthenticated; + getAuthHeaders: GetAuthHeaders; }; + /** + * Flag showing whether a server was configured to use TLS connection. + */ isTlsEnabled: boolean; } diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index eff7de0979c119..9730a25610fe0a 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -19,13 +19,25 @@ export { config, HttpConfig, HttpConfigType } from './http_config'; export { HttpService, HttpServiceSetup, HttpServiceStart } from './http_service'; +export { HttpServerSetup } from './http_server'; export { GetAuthHeaders } from './auth_headers_storage'; +export { AuthStatus, GetAuthState, IsAuthenticated } from './auth_state_storage'; export { + CustomHttpResponseOptions, isRealRequest, + HttpResponseOptions, + HttpResponsePayload, KibanaRequest, KibanaRequestRoute, + KnownHeaders, + LegacyRequest, + RedirectResponseOptions, + RequestHandler, ResponseError, ResponseErrorMeta, + responseFactory, + ResponseFactory, + RouteConfig, Router, RouteMethod, RouteConfigOptions, @@ -40,3 +52,4 @@ export { } from './lifecycle/auth'; export { OnPostAuthHandler, OnPostAuthToolkit } from './lifecycle/on_post_auth'; export { SessionStorageFactory, SessionStorage } from './session_storage'; +export { SessionStorageCookieOptions } from './cookie_session_storage'; diff --git a/src/core/server/http/router/headers.ts b/src/core/server/http/router/headers.ts index 3d5fe213f92afd..19eaee50819966 100644 --- a/src/core/server/http/router/headers.ts +++ b/src/core/server/http/router/headers.ts @@ -16,12 +16,49 @@ * specific language governing permissions and limitations * under the License. */ +import { IncomingHttpHeaders } from 'http'; import { pick } from '../../../utils'; -/** @public */ -export type Headers = Record; -export type ResponseHeaders = Record; +/** + * Creates a Union type of all known keys of a given interface. + * @example + * ```ts + * interface Person { + * name: string; + * age: number; + * [attributes: string]: string | number; + * } + * type PersonKnownKeys = KnownKeys; // "age" | "name" + * ``` + */ +type KnownKeys = { + [K in keyof T]: string extends K ? never : number extends K ? never : K; +} extends { [_ in keyof T]: infer U } + ? U + : never; + +/** + * Set of well-known HTTP headers. + * @public + */ +export type KnownHeaders = KnownKeys; + +/** + * Http request headers to read. + * @public + */ +export type Headers = { [header in KnownHeaders]?: string | string[] | undefined } & { + [header: string]: string | string[] | undefined; +}; + +/** + * Http response headers to set. + * @public + */ +export type ResponseHeaders = { [header in KnownHeaders]?: string | string[] } & { + [header: string]: string | string[]; +}; const normalizeHeaderField = (field: string) => field.trim().toLowerCase(); diff --git a/src/core/server/http/router/index.ts b/src/core/server/http/router/index.ts index eefa74cee0802e..29fdfa9c03e1cc 100644 --- a/src/core/server/http/router/index.ts +++ b/src/core/server/http/router/index.ts @@ -17,8 +17,23 @@ * under the License. */ -export { Headers, filterHeaders, ResponseHeaders } from './headers'; -export { Router } from './router'; -export { KibanaRequest, KibanaRequestRoute, ensureRawRequest, isRealRequest } from './request'; -export { RouteMethod, RouteConfigOptions } from './route'; -export { ResponseError, ResponseErrorMeta } from './response'; +export { Headers, filterHeaders, ResponseHeaders, KnownHeaders } from './headers'; +export { Router, RequestHandler } from './router'; +export { + KibanaRequest, + KibanaRequestRoute, + isRealRequest, + LegacyRequest, + ensureRawRequest, +} from './request'; +export { RouteMethod, RouteConfig, RouteConfigOptions } from './route'; +export { + CustomHttpResponseOptions, + HttpResponseOptions, + HttpResponsePayload, + RedirectResponseOptions, + ResponseError, + ResponseErrorMeta, + responseFactory, + ResponseFactory, +} from './response'; diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index da10a6500ccc86..4eac2e98317fcc 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -38,16 +38,22 @@ export interface KibanaRequestRoute { options: Required; } +/** + * @deprecated + * `hapi` request object, supported during migration process only for backward compatibility. + * @public + */ +export interface LegacyRequest extends Request {} // eslint-disable-line @typescript-eslint/no-empty-interface + /** * Kibana specific abstraction for an incoming request. * @public - * */ + */ export class KibanaRequest { /** * Factory for creating requests. Validates the request before creating an * instance of a KibanaRequest. * @internal - * */ public static from

( req: Request, @@ -68,6 +74,7 @@ export class KibanaRequest { * Validates the different parts of a request based on the schemas defined for * the route. Builds up the actual params, query and body object that will be * received in the route handler. + * @internal */ private static validate

( req: Request, @@ -102,8 +109,9 @@ export class KibanaRequest { return { query, params, body }; } - + /** a WHATWG URL standard object. */ public readonly url: Url; + /** matched route details */ public readonly route: RecursiveReadonly; /** * Readonly copy of incoming request headers. @@ -153,14 +161,14 @@ export class KibanaRequest { * Returns underlying Hapi Request * @internal */ -export const ensureRawRequest = (request: KibanaRequest | Request) => +export const ensureRawRequest = (request: KibanaRequest | LegacyRequest) => isKibanaRequest(request) ? request[requestSymbol] : request; function isKibanaRequest(request: unknown): request is KibanaRequest { return request instanceof KibanaRequest; } -function isRequest(request: any): request is Request { +function isRequest(request: any): request is LegacyRequest { try { return request.raw.req && typeof request.raw.req === 'object'; } catch { @@ -172,6 +180,6 @@ function isRequest(request: any): request is Request { * Checks if an incoming request either KibanaRequest or Legacy.Request * @internal */ -export function isRealRequest(request: unknown): request is KibanaRequest | Request { +export function isRealRequest(request: unknown): request is KibanaRequest | LegacyRequest { return isKibanaRequest(request) || isRequest(request); } diff --git a/src/core/server/http/router/response.ts b/src/core/server/http/router/response.ts index 65db87f8ae8f8e..98c8fd38e2892f 100644 --- a/src/core/server/http/router/response.ts +++ b/src/core/server/http/router/response.ts @@ -16,8 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { IncomingHttpHeaders } from 'http'; import { Stream } from 'stream'; +import { ResponseHeaders } from './headers'; /** * Additional metadata to enhance error output or provide error details. @@ -40,6 +40,10 @@ export type ResponseError = meta?: ResponseErrorMeta; }; +/** + * A response data object, expected to returned as a result of {@link RequestHandler} execution + * @internal + */ export class KibanaResponse { constructor( readonly status: number, @@ -48,51 +52,31 @@ export class KibanaResponse { ) {} } -/** - * Creates a Union type of all known keys of a given interface. - * @example - * ```ts - * interface Person { - * name: string; - * age: number; - * [attributes: string]: string | number; - * } - * type PersonKnownKeys = KnownKeys; // "age" | "name" - * ``` - */ -type KnownKeys = { - [K in keyof T]: string extends K ? never : number extends K ? never : K; -} extends { [_ in keyof T]: infer U } - ? U - : never; - -type KnownHeaders = KnownKeys; /** * HTTP response parameters * @public */ export interface HttpResponseOptions { /** HTTP Headers with additional information about response */ - headers?: { [header in KnownHeaders]?: string | string[] } & { - [header: string]: string | string[]; - }; + headers?: ResponseHeaders; } /** + * Data send to the client as a response payload. * @public */ export type HttpResponsePayload = undefined | string | Record | Buffer | Stream; /** - * HTTP response parameters + * HTTP response parameters for a response with adjustable status code. * @public */ -export interface CustomResponseOptions extends HttpResponseOptions { +export interface CustomHttpResponseOptions extends HttpResponseOptions { statusCode: number; } /** - * HTTP response parameters + * HTTP response parameters for redirection response * @public */ export type RedirectResponseOptions = HttpResponseOptions & { @@ -101,6 +85,11 @@ export type RedirectResponseOptions = HttpResponseOptions & { }; }; +/** + * Set of helpers used to create `KibanaResponse` to form HTTP response on an incoming request. + * Should be returned as a result of {@link RequestHandler} execution. + * @public + */ export const responseFactory = { // Success /** @@ -131,9 +120,9 @@ export const responseFactory = { /** * Creates a response with defined status code and payload. * @param payload - {@link HttpResponsePayload} payload to send to the client - * @param options - {@link CustomResponseOptions} configures HTTP response parameters. + * @param options - {@link CustomHttpResponseOptions} configures HTTP response parameters. */ - custom: (payload: HttpResponsePayload | ResponseError, options: CustomResponseOptions) => { + custom: (payload: HttpResponsePayload | ResponseError, options: CustomHttpResponseOptions) => { if (!options || !options.statusCode) { throw new Error(`options.statusCode is expected to be set. given options: ${options}`); } diff --git a/src/core/server/http/router/route.ts b/src/core/server/http/router/route.ts index caf13343ec96ca..e8053560148293 100644 --- a/src/core/server/http/router/route.ts +++ b/src/core/server/http/router/route.ts @@ -21,18 +21,18 @@ import { ObjectType } from '@kbn/config-schema'; /** * The set of common HTTP methods supported by Kibana routing. * @public - * */ + */ export type RouteMethod = 'get' | 'post' | 'put' | 'delete'; /** - * Route specific configuration. + * Additional route options. * @public - * */ + */ export interface RouteConfigOptions { /** * A flag shows that authentication for a route: - * enabled when true - * disabled when false + * `enabled` when true + * `disabled` when false * * Enabled by default. */ @@ -44,27 +44,58 @@ export interface RouteConfigOptions { tags?: readonly string[]; } +/** + * Route specific configuration. + * @public + */ export interface RouteConfig

{ /** * The endpoint _within_ the router path to register the route. E.g. if the * router is registered at `/elasticsearch` and the route path is `/search`, * the full path for the route is `/elasticsearch/search`. + * Supports: + * - named path segments `path/{name}`. + * - optional path segments `path/{position?}`. + * - multi-segments `path/{coordinates*2}`. + * Segments are accessible within a handler function as `params` property of {@link KibanaRequest} object. + * To have read access to `params` you *must* specify validation schema with {@link RouteConfig.validate}. */ path: string; /** * A schema created with `@kbn/config-schema` that every request will be validated against. - * + * You *must* specify a validation schema to be able to read: + * - url path segments + * - request query + * - request body * To opt out of validating the request, specify `false`. + * @example + * ```ts + * import { schema } from '@kbn/config-schema'; + * router.get({ + * path: 'path/{id}' + * validate: { + * params: schema.object({ + * id: schema.string(), + * }), + * query: schema.object({...}), + * body: schema.object({...}), + * }, + * }) + * ``` */ validate: RouteSchemas | false; + /** + * Additional route options {@link RouteConfigOptions}. + */ options?: RouteConfigOptions; } /** * RouteSchemas contains the schemas for validating the different parts of a * request. + * @public */ export interface RouteSchemas

{ params?: P; diff --git a/src/core/server/http/router/router.ts b/src/core/server/http/router/router.ts index b801aa41754faa..408426a1220d09 100644 --- a/src/core/server/http/router/router.ts +++ b/src/core/server/http/router/router.ts @@ -26,21 +26,37 @@ import { KibanaResponse, ResponseFactory, responseFactory } from './response'; import { RouteConfig, RouteConfigOptions, RouteMethod, RouteSchemas } from './route'; import { HapiResponseAdapter } from './response_adapter'; -export interface RouterRoute { +interface RouterRoute { method: RouteMethod; path: string; options: RouteConfigOptions; handler: (req: Request, responseToolkit: ResponseToolkit, log: Logger) => Promise; } -/** @public */ +/** + * Provides ability to declare a handler function for a particular path and HTTP request method. + * Each route can have only one handler functions, which is executed when the route is matched. + * + * @example + * ```ts + * const router = new Router('my-app'); + * // handler is called when 'my-app/path' resource is requested with `GET` method + * router.get({ path: '/path', validate: false }, (req, res) => res.ok({ content: 'ok' })); + * ``` + * + * @public + * */ export class Router { public routes: Array> = []; - + /** + * @param path - a router path, set as the very first path segment for all registered routes. + */ constructor(readonly path: string) {} /** - * Register a `GET` request with the router + * Register a route handler for `GET` request. + * @param route {@link RouteConfig} - a route configuration. + * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ public get

( route: RouteConfig, @@ -58,7 +74,9 @@ export class Router { } /** - * Register a `POST` request with the router + * Register a route handler for `POST` request. + * @param route {@link RouteConfig} - a route configuration. + * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ public post

( route: RouteConfig, @@ -76,7 +94,9 @@ export class Router { } /** - * Register a `PUT` request with the router + * Register a route handler for `PUT` request. + * @param route {@link RouteConfig} - a route configuration. + * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ public put

( route: RouteConfig, @@ -94,7 +114,9 @@ export class Router { } /** - * Register a `DELETE` request with the router + * Register a route handler for `DELETE` request. + * @param route {@link RouteConfig} - a route configuration. + * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ public delete

( route: RouteConfig, @@ -114,13 +136,14 @@ export class Router { /** * Returns all routes registered with the this router. * @returns List of registered routes. + * @internal */ public getRoutes() { return [...this.routes]; } /** - * Create the schemas for a route + * Create the validation schemas for a route * * @returns Route schemas if `validate` is specified on the route, otherwise * undefined. @@ -176,6 +199,39 @@ export class Router { } } +/** + * A function executed when route path matched requested resource path. + * Request handler is expected to return a result of one of {@link ResponseFactory} functions. + * @param request {@link KibanaRequest} - object containing information about requested resource, + * such as path, method, headers, parameters, query, body, etc. + * @param createResponse {@link ResponseFactory} - a set of helper functions used to respond to a request. + * + * @example + * ```ts + * const router = new Router('my-app'); + * // creates a route handler for GET request on 'my-app/path/{id}' path + * router.get( + * { + * path: 'path/{id}', + * // defines a validation schema for a named segment of the route path + * validate: { + * params: schema.object({ + * id: schema.string(), + * }), + * }, + * }, + * // function to execute to create a responses + * async (request, createResponse) => { + * const data = await findObject(request.params.id); + * // creates a command to respond with 'not found' error + * if (!data) return createResponse.notFound(); + * // creates a command to send found data to the client + * return createResponse.ok(data); + * } + * ); + * ``` + * @public + */ export type RequestHandler

= ( request: KibanaRequest, TypeOf, TypeOf>, createResponse: ResponseFactory diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 2bbcaa58f6af52..14dd92c6ca02df 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -56,27 +56,41 @@ export { ElasticsearchErrorHelpers, APICaller, FakeRequest, - LegacyRequest, } from './elasticsearch'; export { AuthenticationHandler, AuthHeaders, AuthResultParams, + AuthStatus, AuthToolkit, + CustomHttpResponseOptions, GetAuthHeaders, + GetAuthState, + HttpResponseOptions, + HttpResponsePayload, + HttpServerSetup, + IsAuthenticated, KibanaRequest, KibanaRequestRoute, + KnownHeaders, + LegacyRequest, OnPreAuthHandler, OnPreAuthToolkit, OnPostAuthHandler, OnPostAuthToolkit, + RedirectResponseOptions, + RequestHandler, ResponseError, ResponseErrorMeta, + responseFactory, + ResponseFactory, + RouteConfig, Router, RouteMethod, RouteConfigOptions, - SessionStorageFactory, SessionStorage, + SessionStorageCookieOptions, + SessionStorageFactory, } from './http'; export { Logger, LoggerFactory, LogMeta, LogRecord, LogLevel } from './logging'; From 3198ff1b9ffffc4b8c3b6d535fc54d1e3a43d7e4 Mon Sep 17 00:00:00 2001 From: restrry Date: Tue, 30 Jul 2019 14:24:26 +0200 Subject: [PATCH 14/15] re-generate docs --- .../server/kibana-plugin-server.authstatus.md | 22 +++ ...plugin-server.customhttpresponseoptions.md | 20 +++ ...er.customhttpresponseoptions.statuscode.md | 11 ++ .../kibana-plugin-server.getauthheaders.md | 2 +- .../kibana-plugin-server.getauthstate.md | 16 +++ .../server/kibana-plugin-server.headers.md | 7 +- ...ugin-server.httpresponseoptions.headers.md | 13 ++ ...ibana-plugin-server.httpresponseoptions.md | 20 +++ ...ibana-plugin-server.httpresponsepayload.md | 13 ++ ...bana-plugin-server.httpserversetup.auth.md | 15 ++ ...-plugin-server.httpserversetup.basepath.md | 16 +++ ...setup.createcookiesessionstoragefactory.md | 13 ++ ...gin-server.httpserversetup.istlsenabled.md | 13 ++ .../kibana-plugin-server.httpserversetup.md | 27 ++++ ...gin-server.httpserversetup.registerauth.md | 13 ++ ...rver.httpserversetup.registeronpostauth.md | 13 ++ ...erver.httpserversetup.registeronpreauth.md | 13 ++ ...n-server.httpserversetup.registerrouter.md | 13 ++ ...na-plugin-server.httpserversetup.server.md | 11 ++ .../kibana-plugin-server.isauthenticated.md | 13 ++ .../kibana-plugin-server.kibanarequest.md | 4 +- ...ibana-plugin-server.kibanarequest.route.md | 2 + .../kibana-plugin-server.kibanarequest.url.md | 2 + .../kibana-plugin-server.knownheaders.md | 13 ++ .../kibana-plugin-server.legacyrequest.md | 9 +- .../core/server/kibana-plugin-server.md | 32 ++++- ...a-plugin-server.redirectresponseoptions.md | 17 +++ .../kibana-plugin-server.requesthandler.md | 42 ++++++ .../kibana-plugin-server.responsefactory.md | 13 ++ .../kibana-plugin-server.routeconfig.md | 22 +++ ...ibana-plugin-server.routeconfig.options.md | 13 ++ .../kibana-plugin-server.routeconfig.path.md | 13 ++ ...bana-plugin-server.routeconfig.validate.md | 32 +++++ ...-server.routeconfigoptions.authrequired.md | 2 +- ...kibana-plugin-server.routeconfigoptions.md | 4 +- ...bana-plugin-server.router.(constructor).md | 2 +- .../kibana-plugin-server.router.delete.md | 2 +- .../server/kibana-plugin-server.router.get.md | 2 +- .../kibana-plugin-server.router.getroutes.md | 19 --- .../server/kibana-plugin-server.router.md | 20 ++- .../kibana-plugin-server.router.post.md | 2 +- .../server/kibana-plugin-server.router.put.md | 2 +- ...erver.scopedclusterclient.(constructor).md | 4 +- ...ssionstoragecookieoptions.encryptionkey.md | 13 ++ ...er.sessionstoragecookieoptions.issecure.md | 13 ++ ...ugin-server.sessionstoragecookieoptions.md | 23 +++ ...server.sessionstoragecookieoptions.name.md | 13 ++ ...er.sessionstoragecookieoptions.validate.md | 13 ++ src/core/server/server.api.md | 134 ++++++++++++++++-- 49 files changed, 710 insertions(+), 56 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-server.authstatus.md create mode 100644 docs/development/core/server/kibana-plugin-server.customhttpresponseoptions.md create mode 100644 docs/development/core/server/kibana-plugin-server.customhttpresponseoptions.statuscode.md create mode 100644 docs/development/core/server/kibana-plugin-server.getauthstate.md create mode 100644 docs/development/core/server/kibana-plugin-server.httpresponseoptions.headers.md create mode 100644 docs/development/core/server/kibana-plugin-server.httpresponseoptions.md create mode 100644 docs/development/core/server/kibana-plugin-server.httpresponsepayload.md create mode 100644 docs/development/core/server/kibana-plugin-server.httpserversetup.auth.md create mode 100644 docs/development/core/server/kibana-plugin-server.httpserversetup.basepath.md create mode 100644 docs/development/core/server/kibana-plugin-server.httpserversetup.createcookiesessionstoragefactory.md create mode 100644 docs/development/core/server/kibana-plugin-server.httpserversetup.istlsenabled.md create mode 100644 docs/development/core/server/kibana-plugin-server.httpserversetup.md create mode 100644 docs/development/core/server/kibana-plugin-server.httpserversetup.registerauth.md create mode 100644 docs/development/core/server/kibana-plugin-server.httpserversetup.registeronpostauth.md create mode 100644 docs/development/core/server/kibana-plugin-server.httpserversetup.registeronpreauth.md create mode 100644 docs/development/core/server/kibana-plugin-server.httpserversetup.registerrouter.md create mode 100644 docs/development/core/server/kibana-plugin-server.httpserversetup.server.md create mode 100644 docs/development/core/server/kibana-plugin-server.isauthenticated.md create mode 100644 docs/development/core/server/kibana-plugin-server.knownheaders.md create mode 100644 docs/development/core/server/kibana-plugin-server.redirectresponseoptions.md create mode 100644 docs/development/core/server/kibana-plugin-server.requesthandler.md create mode 100644 docs/development/core/server/kibana-plugin-server.responsefactory.md create mode 100644 docs/development/core/server/kibana-plugin-server.routeconfig.md create mode 100644 docs/development/core/server/kibana-plugin-server.routeconfig.options.md create mode 100644 docs/development/core/server/kibana-plugin-server.routeconfig.path.md create mode 100644 docs/development/core/server/kibana-plugin-server.routeconfig.validate.md delete mode 100644 docs/development/core/server/kibana-plugin-server.router.getroutes.md create mode 100644 docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.encryptionkey.md create mode 100644 docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.issecure.md create mode 100644 docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.md create mode 100644 docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.name.md create mode 100644 docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.validate.md diff --git a/docs/development/core/server/kibana-plugin-server.authstatus.md b/docs/development/core/server/kibana-plugin-server.authstatus.md new file mode 100644 index 00000000000000..e59ade4f73e38b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authstatus.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthStatus](./kibana-plugin-server.authstatus.md) + +## AuthStatus enum + +Status indicating an outcome of the authentication. + +Signature: + +```typescript +export declare enum AuthStatus +``` + +## Enumeration Members + +| Member | Value | Description | +| --- | --- | --- | +| authenticated | "authenticated" | auth interceptor successfully authenticated a user | +| unauthenticated | "unauthenticated" | auth interceptor failed user authentication | +| unknown | "unknown" | auth interceptor has not been registered | + diff --git a/docs/development/core/server/kibana-plugin-server.customhttpresponseoptions.md b/docs/development/core/server/kibana-plugin-server.customhttpresponseoptions.md new file mode 100644 index 00000000000000..cabee8a47e5ca0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.customhttpresponseoptions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CustomHttpResponseOptions](./kibana-plugin-server.customhttpresponseoptions.md) + +## CustomHttpResponseOptions interface + +HTTP response parameters for a response with adjustable status code. + +Signature: + +```typescript +export interface CustomHttpResponseOptions extends HttpResponseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [statusCode](./kibana-plugin-server.customhttpresponseoptions.statuscode.md) | number | | + diff --git a/docs/development/core/server/kibana-plugin-server.customhttpresponseoptions.statuscode.md b/docs/development/core/server/kibana-plugin-server.customhttpresponseoptions.statuscode.md new file mode 100644 index 00000000000000..5444ccd2ebb554 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.customhttpresponseoptions.statuscode.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CustomHttpResponseOptions](./kibana-plugin-server.customhttpresponseoptions.md) > [statusCode](./kibana-plugin-server.customhttpresponseoptions.statuscode.md) + +## CustomHttpResponseOptions.statusCode property + +Signature: + +```typescript +statusCode: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.getauthheaders.md b/docs/development/core/server/kibana-plugin-server.getauthheaders.md index ee7572615fe1a9..fba8b8ca8ee3a4 100644 --- a/docs/development/core/server/kibana-plugin-server.getauthheaders.md +++ b/docs/development/core/server/kibana-plugin-server.getauthheaders.md @@ -9,5 +9,5 @@ Get headers to authenticate a user against Elasticsearch. Signature: ```typescript -export declare type GetAuthHeaders = (request: KibanaRequest | Request) => AuthHeaders | undefined; +export declare type GetAuthHeaders = (request: KibanaRequest | LegacyRequest) => AuthHeaders | undefined; ``` diff --git a/docs/development/core/server/kibana-plugin-server.getauthstate.md b/docs/development/core/server/kibana-plugin-server.getauthstate.md new file mode 100644 index 00000000000000..47fc38c28f5e0a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.getauthstate.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [GetAuthState](./kibana-plugin-server.getauthstate.md) + +## GetAuthState type + +Get authentication state for a request. Returned by `auth` interceptor. + +Signature: + +```typescript +export declare type GetAuthState = (request: KibanaRequest | LegacyRequest) => { + status: AuthStatus; + state: unknown; +}; +``` diff --git a/docs/development/core/server/kibana-plugin-server.headers.md b/docs/development/core/server/kibana-plugin-server.headers.md index 83259efe8b79d5..cd73d4de43b9db 100644 --- a/docs/development/core/server/kibana-plugin-server.headers.md +++ b/docs/development/core/server/kibana-plugin-server.headers.md @@ -4,9 +4,14 @@ ## Headers type +Http request headers to read. Signature: ```typescript -export declare type Headers = Record; +export declare type Headers = { + [header in KnownHeaders]?: string | string[] | undefined; +} & { + [header: string]: string | string[] | undefined; +}; ``` diff --git a/docs/development/core/server/kibana-plugin-server.httpresponseoptions.headers.md b/docs/development/core/server/kibana-plugin-server.httpresponseoptions.headers.md new file mode 100644 index 00000000000000..ee347f99a41a46 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpresponseoptions.headers.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpResponseOptions](./kibana-plugin-server.httpresponseoptions.md) > [headers](./kibana-plugin-server.httpresponseoptions.headers.md) + +## HttpResponseOptions.headers property + +HTTP Headers with additional information about response + +Signature: + +```typescript +headers?: ResponseHeaders; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpresponseoptions.md b/docs/development/core/server/kibana-plugin-server.httpresponseoptions.md new file mode 100644 index 00000000000000..8f9ccf22c8c6dc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpresponseoptions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpResponseOptions](./kibana-plugin-server.httpresponseoptions.md) + +## HttpResponseOptions interface + +HTTP response parameters + +Signature: + +```typescript +export interface HttpResponseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [headers](./kibana-plugin-server.httpresponseoptions.headers.md) | ResponseHeaders | HTTP Headers with additional information about response | + diff --git a/docs/development/core/server/kibana-plugin-server.httpresponsepayload.md b/docs/development/core/server/kibana-plugin-server.httpresponsepayload.md new file mode 100644 index 00000000000000..3dc4e2c7956f75 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpresponsepayload.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpResponsePayload](./kibana-plugin-server.httpresponsepayload.md) + +## HttpResponsePayload type + +Data send to the client as a response payload. + +Signature: + +```typescript +export declare type HttpResponsePayload = undefined | string | Record | Buffer | Stream; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserversetup.auth.md b/docs/development/core/server/kibana-plugin-server.httpserversetup.auth.md new file mode 100644 index 00000000000000..e39c3c63167686 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserversetup.auth.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerSetup](./kibana-plugin-server.httpserversetup.md) > [auth](./kibana-plugin-server.httpserversetup.auth.md) + +## HttpServerSetup.auth property + +Signature: + +```typescript +auth: { + get: GetAuthState; + isAuthenticated: IsAuthenticated; + getAuthHeaders: GetAuthHeaders; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserversetup.basepath.md b/docs/development/core/server/kibana-plugin-server.httpserversetup.basepath.md new file mode 100644 index 00000000000000..5cfb2f5c4e8b43 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserversetup.basepath.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerSetup](./kibana-plugin-server.httpserversetup.md) > [basePath](./kibana-plugin-server.httpserversetup.basepath.md) + +## HttpServerSetup.basePath property + +Signature: + +```typescript +basePath: { + get: (request: KibanaRequest | LegacyRequest) => string; + set: (request: KibanaRequest | LegacyRequest, basePath: string) => void; + prepend: (url: string) => string; + remove: (url: string) => string; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserversetup.createcookiesessionstoragefactory.md b/docs/development/core/server/kibana-plugin-server.httpserversetup.createcookiesessionstoragefactory.md new file mode 100644 index 00000000000000..3dc01a52a2f586 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserversetup.createcookiesessionstoragefactory.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerSetup](./kibana-plugin-server.httpserversetup.md) > [createCookieSessionStorageFactory](./kibana-plugin-server.httpserversetup.createcookiesessionstoragefactory.md) + +## HttpServerSetup.createCookieSessionStorageFactory property + +Creates cookie based session storage factory [SessionStorageFactory](./kibana-plugin-server.sessionstoragefactory.md) + +Signature: + +```typescript +createCookieSessionStorageFactory: (cookieOptions: SessionStorageCookieOptions) => Promise>; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserversetup.istlsenabled.md b/docs/development/core/server/kibana-plugin-server.httpserversetup.istlsenabled.md new file mode 100644 index 00000000000000..6961d4feeb7c7c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserversetup.istlsenabled.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerSetup](./kibana-plugin-server.httpserversetup.md) > [isTlsEnabled](./kibana-plugin-server.httpserversetup.istlsenabled.md) + +## HttpServerSetup.isTlsEnabled property + +Flag showing whether a server was configured to use TLS connection. + +Signature: + +```typescript +isTlsEnabled: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserversetup.md b/docs/development/core/server/kibana-plugin-server.httpserversetup.md new file mode 100644 index 00000000000000..b507bf937eb215 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserversetup.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerSetup](./kibana-plugin-server.httpserversetup.md) + +## HttpServerSetup interface + + +Signature: + +```typescript +export interface HttpServerSetup +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [auth](./kibana-plugin-server.httpserversetup.auth.md) | {
get: GetAuthState;
isAuthenticated: IsAuthenticated;
getAuthHeaders: GetAuthHeaders;
} | | +| [basePath](./kibana-plugin-server.httpserversetup.basepath.md) | {
get: (request: KibanaRequest | LegacyRequest) => string;
set: (request: KibanaRequest | LegacyRequest, basePath: string) => void;
prepend: (url: string) => string;
remove: (url: string) => string;
} | | +| [createCookieSessionStorageFactory](./kibana-plugin-server.httpserversetup.createcookiesessionstoragefactory.md) | <T>(cookieOptions: SessionStorageCookieOptions<T>) => Promise<SessionStorageFactory<T>> | Creates cookie based session storage factory [SessionStorageFactory](./kibana-plugin-server.sessionstoragefactory.md) | +| [isTlsEnabled](./kibana-plugin-server.httpserversetup.istlsenabled.md) | boolean | Flag showing whether a server was configured to use TLS connection. | +| [registerAuth](./kibana-plugin-server.httpserversetup.registerauth.md) | (handler: AuthenticationHandler) => void | To define custom authentication and/or authorization mechanism for incoming requests. A handler should return a state to associate with the incoming request. The state can be retrieved later via http.auth.get(..) Only one AuthenticationHandler can be registered. | +| [registerOnPostAuth](./kibana-plugin-server.httpserversetup.registeronpostauth.md) | (handler: OnPostAuthHandler) => void | To define custom logic to perform for incoming requests. Runs the handler after Auth interceptor did make sure a user has access to the requested resource. The auth state is available at stage via http.auth.get(..) Can register any number of registerOnPreAuth, which are called in sequence (from the first registered to the last). | +| [registerOnPreAuth](./kibana-plugin-server.httpserversetup.registeronpreauth.md) | (handler: OnPreAuthHandler) => void | To define custom logic to perform for incoming requests. Runs the handler before Auth interceptor performs a check that user has access to requested resources, so it's the only place when you can forward a request to another URL right on the server. Can register any number of registerOnPostAuth, which are called in sequence (from the first registered to the last). | +| [registerRouter](./kibana-plugin-server.httpserversetup.registerrouter.md) | (router: Router) => void | Add all the routes registered with router to HTTP server request listeners. | +| [server](./kibana-plugin-server.httpserversetup.server.md) | Server | | + diff --git a/docs/development/core/server/kibana-plugin-server.httpserversetup.registerauth.md b/docs/development/core/server/kibana-plugin-server.httpserversetup.registerauth.md new file mode 100644 index 00000000000000..6e63e0996a63a9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserversetup.registerauth.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerSetup](./kibana-plugin-server.httpserversetup.md) > [registerAuth](./kibana-plugin-server.httpserversetup.registerauth.md) + +## HttpServerSetup.registerAuth property + +To define custom authentication and/or authorization mechanism for incoming requests. A handler should return a state to associate with the incoming request. The state can be retrieved later via http.auth.get(..) Only one AuthenticationHandler can be registered. + +Signature: + +```typescript +registerAuth: (handler: AuthenticationHandler) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserversetup.registeronpostauth.md b/docs/development/core/server/kibana-plugin-server.httpserversetup.registeronpostauth.md new file mode 100644 index 00000000000000..c74a67da350ecc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserversetup.registeronpostauth.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerSetup](./kibana-plugin-server.httpserversetup.md) > [registerOnPostAuth](./kibana-plugin-server.httpserversetup.registeronpostauth.md) + +## HttpServerSetup.registerOnPostAuth property + +To define custom logic to perform for incoming requests. Runs the handler after Auth interceptor did make sure a user has access to the requested resource. The auth state is available at stage via http.auth.get(..) Can register any number of registerOnPreAuth, which are called in sequence (from the first registered to the last). + +Signature: + +```typescript +registerOnPostAuth: (handler: OnPostAuthHandler) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserversetup.registeronpreauth.md b/docs/development/core/server/kibana-plugin-server.httpserversetup.registeronpreauth.md new file mode 100644 index 00000000000000..f6efa1c1dd73c4 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserversetup.registeronpreauth.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerSetup](./kibana-plugin-server.httpserversetup.md) > [registerOnPreAuth](./kibana-plugin-server.httpserversetup.registeronpreauth.md) + +## HttpServerSetup.registerOnPreAuth property + +To define custom logic to perform for incoming requests. Runs the handler before Auth interceptor performs a check that user has access to requested resources, so it's the only place when you can forward a request to another URL right on the server. Can register any number of registerOnPostAuth, which are called in sequence (from the first registered to the last). + +Signature: + +```typescript +registerOnPreAuth: (handler: OnPreAuthHandler) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserversetup.registerrouter.md b/docs/development/core/server/kibana-plugin-server.httpserversetup.registerrouter.md new file mode 100644 index 00000000000000..4c2a9ae3274068 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserversetup.registerrouter.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerSetup](./kibana-plugin-server.httpserversetup.md) > [registerRouter](./kibana-plugin-server.httpserversetup.registerrouter.md) + +## HttpServerSetup.registerRouter property + +Add all the routes registered with `router` to HTTP server request listeners. + +Signature: + +```typescript +registerRouter: (router: Router) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserversetup.server.md b/docs/development/core/server/kibana-plugin-server.httpserversetup.server.md new file mode 100644 index 00000000000000..a137eba7c8a5a7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserversetup.server.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerSetup](./kibana-plugin-server.httpserversetup.md) > [server](./kibana-plugin-server.httpserversetup.server.md) + +## HttpServerSetup.server property + +Signature: + +```typescript +server: Server; +``` diff --git a/docs/development/core/server/kibana-plugin-server.isauthenticated.md b/docs/development/core/server/kibana-plugin-server.isauthenticated.md new file mode 100644 index 00000000000000..15f412710412a2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.isauthenticated.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IsAuthenticated](./kibana-plugin-server.isauthenticated.md) + +## IsAuthenticated type + +Return authentication status for a request. + +Signature: + +```typescript +export declare type IsAuthenticated = (request: KibanaRequest | LegacyRequest) => boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.md index a9622e4319d573..19167f2f640418 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequest.md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.md @@ -26,6 +26,6 @@ export declare class KibanaRequestHeaders | Readonly copy of incoming request headers. | | [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> | | -| [url](./kibana-plugin-server.kibanarequest.url.md) | | Url | | +| [route](./kibana-plugin-server.kibanarequest.route.md) | | RecursiveReadonly<KibanaRequestRoute> | matched route details | +| [url](./kibana-plugin-server.kibanarequest.url.md) | | Url | a WHATWG URL standard object. | diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.route.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.route.md index 301eeef1b6bb58..88954eedf4cfb5 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequest.route.md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.route.md @@ -4,6 +4,8 @@ ## KibanaRequest.route property +matched route details + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.url.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.url.md index b8bd46199763ee..62d1f971594764 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequest.url.md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.url.md @@ -4,6 +4,8 @@ ## KibanaRequest.url property +a WHATWG URL standard object. + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.knownheaders.md b/docs/development/core/server/kibana-plugin-server.knownheaders.md new file mode 100644 index 00000000000000..986794f3aaa61d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.knownheaders.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [KnownHeaders](./kibana-plugin-server.knownheaders.md) + +## KnownHeaders type + +Set of well-known HTTP headers. + +Signature: + +```typescript +export declare type KnownHeaders = KnownKeys; +``` diff --git a/docs/development/core/server/kibana-plugin-server.legacyrequest.md b/docs/development/core/server/kibana-plugin-server.legacyrequest.md index 6f67928faa52cf..a794b3bbe87c7d 100644 --- a/docs/development/core/server/kibana-plugin-server.legacyrequest.md +++ b/docs/development/core/server/kibana-plugin-server.legacyrequest.md @@ -2,12 +2,15 @@ [Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [LegacyRequest](./kibana-plugin-server.legacyrequest.md) -## LegacyRequest type +## LegacyRequest interface -Support Legacy platform request for the period of migration. +> Warning: This API is now obsolete. +> +> `hapi` request object, supported during migration process only for backward compatibility. +> Signature: ```typescript -export declare type LegacyRequest = Request; +export interface LegacyRequest extends Request ``` diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index bd4ef847230575..b08cc8a9457992 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -17,12 +17,18 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ClusterClient](./kibana-plugin-server.clusterclient.md) | Represents an Elasticsearch cluster API client and allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)). | | [ElasticsearchErrorHelpers](./kibana-plugin-server.elasticsearcherrorhelpers.md) | Helpers for working with errors returned from the Elasticsearch service.Since the internal data of errors are subject to change, consumers of the Elasticsearch service should always use these helpers to classify errors instead of checking error internals such as body.error.header[WWW-Authenticate] | | [KibanaRequest](./kibana-plugin-server.kibanarequest.md) | Kibana specific abstraction for an incoming request. | -| [Router](./kibana-plugin-server.router.md) | | +| [Router](./kibana-plugin-server.router.md) | Provides ability to declare a handler function for a particular path and HTTP request method. Each route can have only one handler functions, which is executed when the route is matched. | | [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | | | [SavedObjectsSchema](./kibana-plugin-server.savedobjectsschema.md) | | | [SavedObjectsSerializer](./kibana-plugin-server.savedobjectsserializer.md) | | | [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md) | Serves the same purpose as "normal" ClusterClient but exposes additional callAsCurrentUser method that doesn't use credentials of the Kibana internal user (as callAsInternalUser does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API | +## Enumerations + +| Enumeration | Description | +| --- | --- | +| [AuthStatus](./kibana-plugin-server.authstatus.md) | Status indicating an outcome of the authentication. | + ## Interfaces | Interface | Description | @@ -32,14 +38,18 @@ The plugin integrates with the core system via lifecycle events: `setup` | [CallAPIOptions](./kibana-plugin-server.callapioptions.md) | The set of options that defines how API call should be made and result be processed. | | [CoreSetup](./kibana-plugin-server.coresetup.md) | Context passed to the plugins setup method. | | [CoreStart](./kibana-plugin-server.corestart.md) | Context passed to the plugins start method. | +| [CustomHttpResponseOptions](./kibana-plugin-server.customhttpresponseoptions.md) | HTTP response parameters for a response with adjustable status code. | | [DiscoveredPlugin](./kibana-plugin-server.discoveredplugin.md) | Small container object used to expose information about discovered plugins that may or may not have been started. | | [ElasticsearchError](./kibana-plugin-server.elasticsearcherror.md) | | | [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) | | | [FakeRequest](./kibana-plugin-server.fakerequest.md) | Fake request object created manually by Kibana plugins. | +| [HttpResponseOptions](./kibana-plugin-server.httpresponseoptions.md) | HTTP response parameters | +| [HttpServerSetup](./kibana-plugin-server.httpserversetup.md) | | | [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | | | [HttpServiceStart](./kibana-plugin-server.httpservicestart.md) | | | [InternalCoreStart](./kibana-plugin-server.internalcorestart.md) | | | [KibanaRequestRoute](./kibana-plugin-server.kibanarequestroute.md) | Request specific route information exposed to a handler. | +| [LegacyRequest](./kibana-plugin-server.legacyrequest.md) | | | [Logger](./kibana-plugin-server.logger.md) | Logger exposes all the necessary methods to log any type of information and this is the interface used by the logging consumers including plugins. | | [LoggerFactory](./kibana-plugin-server.loggerfactory.md) | The single purpose of LoggerFactory interface is to define a way to retrieve a context-based logger instance. | | [LogMeta](./kibana-plugin-server.logmeta.md) | Contextual metadata | @@ -50,7 +60,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) | | | [PluginsServiceStart](./kibana-plugin-server.pluginsservicestart.md) | | | [ResponseErrorMeta](./kibana-plugin-server.responseerrormeta.md) | Additional metadata to enhance error output or provide error details. | -| [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md) | Route specific configuration. | +| [RouteConfig](./kibana-plugin-server.routeconfig.md) | Route specific configuration. | +| [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md) | Additional route options. | | [SavedObject](./kibana-plugin-server.savedobject.md) | | | [SavedObjectAttributes](./kibana-plugin-server.savedobjectattributes.md) | | | [SavedObjectReference](./kibana-plugin-server.savedobjectreference.md) | A reference to another saved object. | @@ -68,8 +79,15 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsUpdateOptions](./kibana-plugin-server.savedobjectsupdateoptions.md) | | | [SavedObjectsUpdateResponse](./kibana-plugin-server.savedobjectsupdateresponse.md) | | | [SessionStorage](./kibana-plugin-server.sessionstorage.md) | Provides an interface to store and retrieve data across requests. | +| [SessionStorageCookieOptions](./kibana-plugin-server.sessionstoragecookieoptions.md) | Configuration used to create HTTP session storage based on top of cookie mechanism. | | [SessionStorageFactory](./kibana-plugin-server.sessionstoragefactory.md) | SessionStorage factory to bind one to an incoming request | +## Variables + +| Variable | Description | +| --- | --- | +| [responseFactory](./kibana-plugin-server.responsefactory.md) | Set of helpers used to create KibanaResponse to form HTTP response on an incoming request. Should be returned as a result of [RequestHandler](./kibana-plugin-server.requesthandler.md) execution. | + ## Type Aliases | Type Alias | Description | @@ -79,14 +97,20 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AuthHeaders](./kibana-plugin-server.authheaders.md) | Auth Headers map | | [ElasticsearchClientConfig](./kibana-plugin-server.elasticsearchclientconfig.md) | | | [GetAuthHeaders](./kibana-plugin-server.getauthheaders.md) | Get headers to authenticate a user against Elasticsearch. | -| [Headers](./kibana-plugin-server.headers.md) | | -| [LegacyRequest](./kibana-plugin-server.legacyrequest.md) | Support Legacy platform request for the period of migration. | +| [GetAuthState](./kibana-plugin-server.getauthstate.md) | Get authentication state for a request. Returned by auth interceptor. | +| [Headers](./kibana-plugin-server.headers.md) | Http request headers to read. | +| [HttpResponsePayload](./kibana-plugin-server.httpresponsepayload.md) | Data send to the client as a response payload. | +| [IsAuthenticated](./kibana-plugin-server.isauthenticated.md) | Return authentication status for a request. | +| [KnownHeaders](./kibana-plugin-server.knownheaders.md) | Set of well-known HTTP headers. | | [OnPostAuthHandler](./kibana-plugin-server.onpostauthhandler.md) | | | [OnPreAuthHandler](./kibana-plugin-server.onpreauthhandler.md) | | | [PluginInitializer](./kibana-plugin-server.plugininitializer.md) | The plugin export at the root of a plugin's server directory should conform to this interface. | | [PluginName](./kibana-plugin-server.pluginname.md) | Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays that use it as a key or value more obvious. | | [RecursiveReadonly](./kibana-plugin-server.recursivereadonly.md) | | +| [RedirectResponseOptions](./kibana-plugin-server.redirectresponseoptions.md) | HTTP response parameters for redirection response | +| [RequestHandler](./kibana-plugin-server.requesthandler.md) | A function executed when route path matched requested resource path. Request handler is expected to return a result of one of [ResponseFactory](./kibana-plugin-server.responsefactory.md) functions. | | [ResponseError](./kibana-plugin-server.responseerror.md) | Error message and optional data send to the client in case of error. | +| [ResponseFactory](./kibana-plugin-server.responsefactory.md) | Creates an object containing request response payload, HTTP headers, error details, and other data transmitted to the client. | | [RouteMethod](./kibana-plugin-server.routemethod.md) | The set of common HTTP methods supported by Kibana routing. | | [SavedObjectsClientContract](./kibana-plugin-server.savedobjectsclientcontract.md) | \#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError() helpers exposed at SavedObjectsErrorHelpers should be used to understand and manage error responses from the SavedObjectsClient.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type or doing substring checks on error.body.error.reason, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.\#\#\# 503s from missing indexUnlike all other methods, create requests are supposed to succeed even when the Kibana index does not exist because it will be automatically created by elasticsearch. When that is not the case it is because Elasticsearch's action.auto_create_index setting prevents it from being created automatically so we throw a special 503 with the intention of informing the user that their Elasticsearch settings need to be updated.See [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | | [SavedObjectsClientWrapperFactory](./kibana-plugin-server.savedobjectsclientwrapperfactory.md) | | diff --git a/docs/development/core/server/kibana-plugin-server.redirectresponseoptions.md b/docs/development/core/server/kibana-plugin-server.redirectresponseoptions.md new file mode 100644 index 00000000000000..6fb0a5add2fb65 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.redirectresponseoptions.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RedirectResponseOptions](./kibana-plugin-server.redirectresponseoptions.md) + +## RedirectResponseOptions type + +HTTP response parameters for redirection response + +Signature: + +```typescript +export declare type RedirectResponseOptions = HttpResponseOptions & { + headers: { + location: string; + }; +}; +``` diff --git a/docs/development/core/server/kibana-plugin-server.requesthandler.md b/docs/development/core/server/kibana-plugin-server.requesthandler.md new file mode 100644 index 00000000000000..7a57ca32e4c6eb --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.requesthandler.md @@ -0,0 +1,42 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RequestHandler](./kibana-plugin-server.requesthandler.md) + +## RequestHandler type + +A function executed when route path matched requested resource path. Request handler is expected to return a result of one of [ResponseFactory](./kibana-plugin-server.responsefactory.md) functions. + +Signature: + +```typescript +export declare type RequestHandler

= (request: KibanaRequest, TypeOf, TypeOf>, createResponse: ResponseFactory) => KibanaResponse | Promise>; +``` + +## Example + + +```ts +const router = new Router('my-app'); +// creates a route handler for GET request on 'my-app/path/{id}' path +router.get( + { + path: 'path/{id}', + // defines a validation schema for a named segment of the route path + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + // function to execute to create a responses + async (request, createResponse) => { + const data = await findObject(request.params.id); + // creates a command to respond with 'not found' error + if (!data) return createResponse.notFound(); + // creates a command to send found data to the client + return createResponse.ok(data); + } +); + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.responsefactory.md b/docs/development/core/server/kibana-plugin-server.responsefactory.md new file mode 100644 index 00000000000000..3b0e5b746c05a5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.responsefactory.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ResponseFactory](./kibana-plugin-server.responsefactory.md) + +## ResponseFactory type + +Creates an object containing request response payload, HTTP headers, error details, and other data transmitted to the client. + +Signature: + +```typescript +export declare type ResponseFactory = typeof responseFactory; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routeconfig.md b/docs/development/core/server/kibana-plugin-server.routeconfig.md new file mode 100644 index 00000000000000..87ec365dc25108 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeconfig.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteConfig](./kibana-plugin-server.routeconfig.md) + +## RouteConfig interface + +Route specific configuration. + +Signature: + +```typescript +export interface RouteConfig

+``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [options](./kibana-plugin-server.routeconfig.options.md) | RouteConfigOptions | Additional route options [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md). | +| [path](./kibana-plugin-server.routeconfig.path.md) | string | The endpoint \_within\_ the router path to register the route. E.g. if the router is registered at /elasticsearch and the route path is /search, the full path for the route is /elasticsearch/search. Supports: - named path segments path/{name}. - optional path segments path/{position?}. - multi-segments path/{coordinates*2}. Segments are accessible within a handler function as params property of [KibanaRequest](./kibana-plugin-server.kibanarequest.md) object. To have read access to params you \*must\* specify validation schema with [RouteConfig.validate](./kibana-plugin-server.routeconfig.validate.md). | +| [validate](./kibana-plugin-server.routeconfig.validate.md) | RouteSchemas<P, Q, B> | false | A schema created with @kbn/config-schema that every request will be validated against. You \*must\* specify a validation schema to be able to read: - url path segments - request query - request body To opt out of validating the request, specify false. | + diff --git a/docs/development/core/server/kibana-plugin-server.routeconfig.options.md b/docs/development/core/server/kibana-plugin-server.routeconfig.options.md new file mode 100644 index 00000000000000..12ca36da6de7cb --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeconfig.options.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteConfig](./kibana-plugin-server.routeconfig.md) > [options](./kibana-plugin-server.routeconfig.options.md) + +## RouteConfig.options property + +Additional route options [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md). + +Signature: + +```typescript +options?: RouteConfigOptions; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routeconfig.path.md b/docs/development/core/server/kibana-plugin-server.routeconfig.path.md new file mode 100644 index 00000000000000..3437f0e0fe0640 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeconfig.path.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteConfig](./kibana-plugin-server.routeconfig.md) > [path](./kibana-plugin-server.routeconfig.path.md) + +## RouteConfig.path property + +The endpoint \_within\_ the router path to register the route. E.g. if the router is registered at `/elasticsearch` and the route path is `/search`, the full path for the route is `/elasticsearch/search`. Supports: - named path segments `path/{name}`. - optional path segments `path/{position?}`. - multi-segments `path/{coordinates*2}`. Segments are accessible within a handler function as `params` property of [KibanaRequest](./kibana-plugin-server.kibanarequest.md) object. To have read access to `params` you \*must\* specify validation schema with [RouteConfig.validate](./kibana-plugin-server.routeconfig.validate.md). + +Signature: + +```typescript +path: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routeconfig.validate.md b/docs/development/core/server/kibana-plugin-server.routeconfig.validate.md new file mode 100644 index 00000000000000..f7177485f5fb60 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeconfig.validate.md @@ -0,0 +1,32 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteConfig](./kibana-plugin-server.routeconfig.md) > [validate](./kibana-plugin-server.routeconfig.validate.md) + +## RouteConfig.validate property + +A schema created with `@kbn/config-schema` that every request will be validated against. You \*must\* specify a validation schema to be able to read: - url path segments - request query - request body To opt out of validating the request, specify `false`. + +Signature: + +```typescript +validate: RouteSchemas | false; +``` + +## Example + + +```ts + import { schema } from '@kbn/config-schema'; + router.get({ + path: 'path/{id}' + validate: { + params: schema.object({ + id: schema.string(), + }), + query: schema.object({...}), + body: schema.object({...}), + }, + }) + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.authrequired.md b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.authrequired.md index 3fb4426c407cd2..2bb2491cae5dfc 100644 --- a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.authrequired.md +++ b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.authrequired.md @@ -4,7 +4,7 @@ ## RouteConfigOptions.authRequired property -A flag shows that authentication for a route: enabled when true disabled when false +A flag shows that authentication for a route: `enabled` when true `disabled` when false Enabled by default. diff --git a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md index 97e480c5490fc5..b4d210ac0b7110 100644 --- a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md +++ b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md @@ -4,7 +4,7 @@ ## RouteConfigOptions interface -Route specific configuration. +Additional route options. Signature: @@ -16,6 +16,6 @@ export interface RouteConfigOptions | Property | Type | Description | | --- | --- | --- | -| [authRequired](./kibana-plugin-server.routeconfigoptions.authrequired.md) | boolean | A flag shows that authentication for a route: enabled when true disabled when falseEnabled by default. | +| [authRequired](./kibana-plugin-server.routeconfigoptions.authrequired.md) | boolean | A flag shows that authentication for a route: enabled when true disabled when falseEnabled by default. | | [tags](./kibana-plugin-server.routeconfigoptions.tags.md) | readonly string[] | Additional metadata tag strings to attach to the route. | diff --git a/docs/development/core/server/kibana-plugin-server.router.(constructor).md b/docs/development/core/server/kibana-plugin-server.router.(constructor).md index 5f8e1e5e293ab7..26048a603c9f6b 100644 --- a/docs/development/core/server/kibana-plugin-server.router.(constructor).md +++ b/docs/development/core/server/kibana-plugin-server.router.(constructor).md @@ -16,5 +16,5 @@ constructor(path: string); | Parameter | Type | Description | | --- | --- | --- | -| path | string | | +| path | string | a router path, set as the very first path segment for all registered routes. | diff --git a/docs/development/core/server/kibana-plugin-server.router.delete.md b/docs/development/core/server/kibana-plugin-server.router.delete.md index cd49f80baaf70d..565dc10ce76e83 100644 --- a/docs/development/core/server/kibana-plugin-server.router.delete.md +++ b/docs/development/core/server/kibana-plugin-server.router.delete.md @@ -4,7 +4,7 @@ ## Router.delete() method -Register a `DELETE` request with the router +Register a route handler for `DELETE` request. Signature: diff --git a/docs/development/core/server/kibana-plugin-server.router.get.md b/docs/development/core/server/kibana-plugin-server.router.get.md index ab8e7c8c5a65d2..a3899eaa678f7c 100644 --- a/docs/development/core/server/kibana-plugin-server.router.get.md +++ b/docs/development/core/server/kibana-plugin-server.router.get.md @@ -4,7 +4,7 @@ ## Router.get() method -Register a `GET` request with the router +Register a route handler for `GET` request. Signature: diff --git a/docs/development/core/server/kibana-plugin-server.router.getroutes.md b/docs/development/core/server/kibana-plugin-server.router.getroutes.md deleted file mode 100644 index 3e4785a3a7c6ca..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.router.getroutes.md +++ /dev/null @@ -1,19 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [getRoutes](./kibana-plugin-server.router.getroutes.md) - -## Router.getRoutes() method - -Returns all routes registered with the this router. - -Signature: - -```typescript -getRoutes(): Readonly[]; -``` -Returns: - -`Readonly[]` - -List of registered routes. - diff --git a/docs/development/core/server/kibana-plugin-server.router.md b/docs/development/core/server/kibana-plugin-server.router.md index 52193bbc553c71..a5eb1c66c4f0c9 100644 --- a/docs/development/core/server/kibana-plugin-server.router.md +++ b/docs/development/core/server/kibana-plugin-server.router.md @@ -4,6 +4,7 @@ ## Router class +Provides ability to declare a handler function for a particular path and HTTP request method. Each route can have only one handler functions, which is executed when the route is matched. Signature: @@ -28,9 +29,18 @@ export declare class Router | Method | Modifiers | Description | | --- | --- | --- | -| [delete(route, handler)](./kibana-plugin-server.router.delete.md) | | Register a DELETE request with the router | -| [get(route, handler)](./kibana-plugin-server.router.get.md) | | Register a GET request with the router | -| [getRoutes()](./kibana-plugin-server.router.getroutes.md) | | Returns all routes registered with the this router. | -| [post(route, handler)](./kibana-plugin-server.router.post.md) | | Register a POST request with the router | -| [put(route, handler)](./kibana-plugin-server.router.put.md) | | Register a PUT request with the router | +| [delete(route, handler)](./kibana-plugin-server.router.delete.md) | | Register a route handler for DELETE request. | +| [get(route, handler)](./kibana-plugin-server.router.get.md) | | Register a route handler for GET request. | +| [post(route, handler)](./kibana-plugin-server.router.post.md) | | Register a route handler for POST request. | +| [put(route, handler)](./kibana-plugin-server.router.put.md) | | Register a route handler for PUT request. | + +## Example + + +```ts +const router = new Router('my-app'); +// handler is called when 'my-app/path' resource is requested with `GET` method +router.get({ path: '/path', validate: false }, (req, res) => res.ok({ content: 'ok' })); + +``` diff --git a/docs/development/core/server/kibana-plugin-server.router.post.md b/docs/development/core/server/kibana-plugin-server.router.post.md index a499a46b1ee79a..7aca35466d643a 100644 --- a/docs/development/core/server/kibana-plugin-server.router.post.md +++ b/docs/development/core/server/kibana-plugin-server.router.post.md @@ -4,7 +4,7 @@ ## Router.post() method -Register a `POST` request with the router +Register a route handler for `POST` request. Signature: diff --git a/docs/development/core/server/kibana-plugin-server.router.put.md b/docs/development/core/server/kibana-plugin-server.router.put.md index 7b1337279cca9a..760ccf9ef88e8d 100644 --- a/docs/development/core/server/kibana-plugin-server.router.put.md +++ b/docs/development/core/server/kibana-plugin-server.router.put.md @@ -4,7 +4,7 @@ ## Router.put() method -Register a `PUT` request with the router +Register a route handler for `PUT` request. Signature: diff --git a/docs/development/core/server/kibana-plugin-server.scopedclusterclient.(constructor).md b/docs/development/core/server/kibana-plugin-server.scopedclusterclient.(constructor).md index 94b49e43a113c8..0fea07320b2f9f 100644 --- a/docs/development/core/server/kibana-plugin-server.scopedclusterclient.(constructor).md +++ b/docs/development/core/server/kibana-plugin-server.scopedclusterclient.(constructor).md @@ -9,7 +9,7 @@ Constructs a new instance of the `ScopedClusterClient` class Signature: ```typescript -constructor(internalAPICaller: APICaller, scopedAPICaller: APICaller, headers?: Record | undefined); +constructor(internalAPICaller: APICaller, scopedAPICaller: APICaller, headers?: Headers | undefined); ``` ## Parameters @@ -18,5 +18,5 @@ constructor(internalAPICaller: APICaller, scopedAPICaller: APICaller, headers?: | --- | --- | --- | | internalAPICaller | APICaller | | | scopedAPICaller | APICaller | | -| headers | Record<string, string | string[] | undefined> | undefined | | +| headers | Headers | undefined | | diff --git a/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.encryptionkey.md b/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.encryptionkey.md new file mode 100644 index 00000000000000..167ab03d7567f5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.encryptionkey.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SessionStorageCookieOptions](./kibana-plugin-server.sessionstoragecookieoptions.md) > [encryptionKey](./kibana-plugin-server.sessionstoragecookieoptions.encryptionkey.md) + +## SessionStorageCookieOptions.encryptionKey property + +A key used to encrypt a cookie value. Should be at least 32 characters long. + +Signature: + +```typescript +encryptionKey: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.issecure.md b/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.issecure.md new file mode 100644 index 00000000000000..824fc9d136a3ff --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.issecure.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SessionStorageCookieOptions](./kibana-plugin-server.sessionstoragecookieoptions.md) > [isSecure](./kibana-plugin-server.sessionstoragecookieoptions.issecure.md) + +## SessionStorageCookieOptions.isSecure property + +Flag indicating whether the cookie should be sent only via a secure connection. + +Signature: + +```typescript +isSecure: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.md b/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.md new file mode 100644 index 00000000000000..de412818142f25 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SessionStorageCookieOptions](./kibana-plugin-server.sessionstoragecookieoptions.md) + +## SessionStorageCookieOptions interface + +Configuration used to create HTTP session storage based on top of cookie mechanism. + +Signature: + +```typescript +export interface SessionStorageCookieOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [encryptionKey](./kibana-plugin-server.sessionstoragecookieoptions.encryptionkey.md) | string | A key used to encrypt a cookie value. Should be at least 32 characters long. | +| [isSecure](./kibana-plugin-server.sessionstoragecookieoptions.issecure.md) | boolean | Flag indicating whether the cookie should be sent only via a secure connection. | +| [name](./kibana-plugin-server.sessionstoragecookieoptions.name.md) | string | Name of the session cookie. | +| [validate](./kibana-plugin-server.sessionstoragecookieoptions.validate.md) | (sessionValue: T) => boolean | Promise<boolean> | Function called to validate a cookie content. | + diff --git a/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.name.md b/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.name.md new file mode 100644 index 00000000000000..e6bc7ea3fe00f1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.name.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SessionStorageCookieOptions](./kibana-plugin-server.sessionstoragecookieoptions.md) > [name](./kibana-plugin-server.sessionstoragecookieoptions.name.md) + +## SessionStorageCookieOptions.name property + +Name of the session cookie. + +Signature: + +```typescript +name: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.validate.md b/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.validate.md new file mode 100644 index 00000000000000..f3cbfc0d84e18e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.validate.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SessionStorageCookieOptions](./kibana-plugin-server.sessionstoragecookieoptions.md) > [validate](./kibana-plugin-server.sessionstoragecookieoptions.validate.md) + +## SessionStorageCookieOptions.validate property + +Function called to validate a cookie content. + +Signature: + +```typescript +validate: (sessionValue: T) => boolean | Promise; +``` diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 6ab535a66cb360..bce7266594f3cf 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -39,6 +39,13 @@ export interface AuthResultParams { state?: Record; } +// @public +export enum AuthStatus { + authenticated = "authenticated", + unauthenticated = "unauthenticated", + unknown = "unknown" +} + // @public export interface AuthToolkit { authenticated: (data?: AuthResultParams) => AuthResult; @@ -109,6 +116,12 @@ export interface CoreSetup { export interface CoreStart { } +// @public +export interface CustomHttpResponseOptions extends HttpResponseOptions { + // (undocumented) + statusCode: number; +} + // @public export interface DiscoveredPlugin { readonly configPath: ConfigPath; @@ -162,13 +175,55 @@ export interface FakeRequest { } // @public -export type GetAuthHeaders = (request: KibanaRequest | Request) => AuthHeaders | undefined; +export type GetAuthHeaders = (request: KibanaRequest | LegacyRequest) => AuthHeaders | undefined; + +// @public +export type GetAuthState = (request: KibanaRequest | LegacyRequest) => { + status: AuthStatus; + state: unknown; +}; + +// @public +export type Headers = { + [header in KnownHeaders]?: string | string[] | undefined; +} & { + [header: string]: string | string[] | undefined; +}; + +// @public +export interface HttpResponseOptions { + // Warning: (ae-forgotten-export) The symbol "ResponseHeaders" needs to be exported by the entry point index.d.ts + headers?: ResponseHeaders; +} + +// @public +export type HttpResponsePayload = undefined | string | Record | Buffer | Stream; // @public (undocumented) -export type Headers = Record; +export interface HttpServerSetup { + // (undocumented) + auth: { + get: GetAuthState; + isAuthenticated: IsAuthenticated; + getAuthHeaders: GetAuthHeaders; + }; + // (undocumented) + basePath: { + get: (request: KibanaRequest | LegacyRequest) => string; + set: (request: KibanaRequest | LegacyRequest, basePath: string) => void; + prepend: (url: string) => string; + remove: (url: string) => string; + }; + createCookieSessionStorageFactory: (cookieOptions: SessionStorageCookieOptions) => Promise>; + isTlsEnabled: boolean; + registerAuth: (handler: AuthenticationHandler) => void; + registerOnPostAuth: (handler: OnPostAuthHandler) => void; + registerOnPreAuth: (handler: OnPreAuthHandler) => void; + registerRouter: (router: Router) => void; + // (undocumented) + server: Server; +} -// Warning: (ae-forgotten-export) The symbol "HttpServerSetup" needs to be exported by the entry point index.d.ts -// // @public (undocumented) export interface HttpServiceSetup extends HttpServerSetup { // Warning: (ae-forgotten-export) The symbol "HttpConfig" needs to be exported by the entry point index.d.ts @@ -198,6 +253,9 @@ export interface InternalCoreStart { plugins: PluginsServiceStart; } +// @public +export type IsAuthenticated = (request: KibanaRequest | LegacyRequest) => boolean; + // @public export class KibanaRequest { // @internal (undocumented) @@ -214,9 +272,7 @@ export class KibanaRequest { readonly params: Params; // (undocumented) readonly query: Query; - // (undocumented) readonly route: RecursiveReadonly; - // (undocumented) readonly url: Url; } @@ -230,8 +286,14 @@ export interface KibanaRequestRoute { path: string; } +// Warning: (ae-forgotten-export) The symbol "KnownKeys" needs to be exported by the entry point index.d.ts +// // @public -export type LegacyRequest = Request; +export type KnownHeaders = KnownKeys; + +// @public @deprecated (undocumented) +export interface LegacyRequest extends Request { +} // @public export interface Logger { @@ -387,6 +449,18 @@ export type RecursiveReadonly = T extends (...args: any[]) => any ? T : T ext [K in keyof T]: RecursiveReadonly; }> : T; +// @public +export type RedirectResponseOptions = HttpResponseOptions & { + headers: { + location: string; + }; +}; + +// Warning: (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts +// +// @public +export type RequestHandler

= (request: KibanaRequest, TypeOf, TypeOf>, createResponse: ResponseFactory) => KibanaResponse | Promise>; + // @public export type ResponseError = string | Error | { message: string | Error; @@ -403,6 +477,37 @@ export interface ResponseErrorMeta { errorCode?: string; } +// @public +export type ResponseFactory = typeof responseFactory; + +// @public +export const responseFactory: { + ok: (payload: HttpResponsePayload, options?: HttpResponseOptions) => KibanaResponse | Buffer | Stream>; + accepted: (payload?: HttpResponsePayload, options?: HttpResponseOptions) => KibanaResponse | Buffer | Stream>; + noContent: (options?: HttpResponseOptions) => KibanaResponse; + custom: (payload: string | Error | Record | Buffer | Stream | { + message: string | Error; + meta?: ResponseErrorMeta | undefined; + } | undefined, options: CustomHttpResponseOptions) => KibanaResponse | Buffer | Stream | { + message: string | Error; + meta?: ResponseErrorMeta | undefined; + }>; + redirected: (payload: HttpResponsePayload, options: RedirectResponseOptions) => KibanaResponse | Buffer | Stream>; + badRequest: (error?: ResponseError, options?: HttpResponseOptions) => KibanaResponse; + unauthorized: (error?: ResponseError, options?: HttpResponseOptions) => KibanaResponse; + forbidden: (error?: ResponseError, options?: HttpResponseOptions) => KibanaResponse; + notFound: (error?: ResponseError, options?: HttpResponseOptions) => KibanaResponse; + conflict: (error?: ResponseError, options?: HttpResponseOptions) => KibanaResponse; + internal: (error?: ResponseError, options?: HttpResponseOptions) => KibanaResponse; +}; + +// @public +export interface RouteConfig

{ + options?: RouteConfigOptions; + path: string; + validate: RouteSchemas | false; +} + // @public export interface RouteConfigOptions { authRequired?: boolean; @@ -412,13 +517,12 @@ export interface RouteConfigOptions { // @public export type RouteMethod = 'get' | 'post' | 'put' | 'delete'; -// @public (undocumented) +// @public export class Router { constructor(path: string); delete

(route: RouteConfig, handler: RequestHandler): void; - // Warning: (ae-forgotten-export) The symbol "RouteConfig" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "RequestHandler" needs to be exported by the entry point index.d.ts get

(route: RouteConfig, handler: RequestHandler): void; + // @internal getRoutes(): Readonly[]; // (undocumented) readonly path: string; @@ -729,7 +833,7 @@ export interface SavedObjectsUpdateResponse | undefined); + constructor(internalAPICaller: APICaller, scopedAPICaller: APICaller, headers?: Headers | undefined); callAsCurrentUser(endpoint: string, clientParams?: Record, options?: CallAPIOptions): Promise; callAsInternalUser(endpoint: string, clientParams?: Record, options?: CallAPIOptions): Promise; } @@ -741,6 +845,14 @@ export interface SessionStorage { set(sessionValue: T): void; } +// @public +export interface SessionStorageCookieOptions { + encryptionKey: string; + isSecure: boolean; + name: string; + validate: (sessionValue: T) => boolean | Promise; +} + // @public export interface SessionStorageFactory { // (undocumented) From 83b63131b862225e4099cc30c7c040e1660a2abf Mon Sep 17 00:00:00 2001 From: restrry Date: Tue, 30 Jul 2019 14:57:18 +0200 Subject: [PATCH 15/15] add link to added section in migration guide --- src/core/MIGRATION.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 400890da3fe764..18570927c20215 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -27,6 +27,7 @@ * [How do I build my shim for New Platform services?](#how-do-i-build-my-shim-for-new-platform-services) * [How to](#how-to) * [Configure plugin](#configure-plugin) + * [Handle HTTP request with New Platform HTTP Service](#handle-http-request-with-new-platform-http-service) * [Mock core services in tests](#mock-core-services-in-tests) Make no mistake, it is going to take a lot of work to move certain plugins to the new platform. Our target is to migrate the entire repo over to the new platform throughout 7.x and to remove the legacy plugin system no later than 8.0, and this is only possible if teams start on the effort now.