Allow routes to define some payload config values (#50783) (#51864)

* Allow routes to define some payload config values

* Documentation typo

* Move hapi `payload` config under `body` + additional validations

* Update API docs

* Amend explanation in API docs

* Add stream and buffer types to @kbn/config-schema

* Fixes based on PR feedback:
- Add 'patch' and 'options' to valid RouteMethod
- Add tests for all the new flags
- Allow `stream` and `buffer` schema in the body validations (findings from tests)

* API documentation update

* Fix type definitions

* Fix the NITs in the PR comments + better typing inheritance

* API docs update

* Fix APM-legacy wrapper's types

* Fix KibanaRequest.from type exposure of hapi in API docs

* Move RouterRoute interface back to private + Expose some public docs

* Update @kbn/config-schema docs
This commit is contained in:
Alejandro Fernández Haro 2019-11-28 10:44:15 +00:00 committed by GitHub
parent 5188fd919f
commit fe7a08c3e2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 1064 additions and 120 deletions

View file

@ -9,5 +9,5 @@ returns `basePath` value, specific for an incoming request.
<b>Signature:</b>
```typescript
get: (request: KibanaRequest<unknown, unknown, unknown> | LegacyRequest) => string;
get: (request: KibanaRequest<unknown, unknown, unknown, any> | LegacyRequest) => string;
```

View file

@ -16,11 +16,11 @@ export declare class BasePath
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [get](./kibana-plugin-server.basepath.get.md) | | <code>(request: KibanaRequest&lt;unknown, unknown, unknown&gt; &#124; LegacyRequest) =&gt; string</code> | returns <code>basePath</code> value, specific for an incoming request. |
| [get](./kibana-plugin-server.basepath.get.md) | | <code>(request: KibanaRequest&lt;unknown, unknown, unknown, any&gt; &#124; LegacyRequest) =&gt; string</code> | returns <code>basePath</code> value, specific for an incoming request. |
| [prepend](./kibana-plugin-server.basepath.prepend.md) | | <code>(path: string) =&gt; string</code> | Prepends <code>path</code> with the basePath. |
| [remove](./kibana-plugin-server.basepath.remove.md) | | <code>(path: string) =&gt; string</code> | Removes the prepended basePath from the <code>path</code>. |
| [serverBasePath](./kibana-plugin-server.basepath.serverbasepath.md) | | <code>string</code> | returns the server's basePath<!-- -->See [BasePath.get](./kibana-plugin-server.basepath.get.md) for getting the basePath value for a specific request |
| [set](./kibana-plugin-server.basepath.set.md) | | <code>(request: KibanaRequest&lt;unknown, unknown, unknown&gt; &#124; LegacyRequest, requestSpecificBasePath: string) =&gt; void</code> | sets <code>basePath</code> value, specific for an incoming request. |
| [set](./kibana-plugin-server.basepath.set.md) | | <code>(request: KibanaRequest&lt;unknown, unknown, unknown, any&gt; &#124; LegacyRequest, requestSpecificBasePath: string) =&gt; void</code> | sets <code>basePath</code> value, specific for an incoming request. |
## Remarks

View file

@ -9,5 +9,5 @@ sets `basePath` value, specific for an incoming request.
<b>Signature:</b>
```typescript
set: (request: KibanaRequest<unknown, unknown, unknown> | LegacyRequest, requestSpecificBasePath: string) => void;
set: (request: KibanaRequest<unknown, unknown, unknown, any> | LegacyRequest, requestSpecificBasePath: string) => void;
```

View file

@ -9,5 +9,5 @@ Register a route handler for `DELETE` request.
<b>Signature:</b>
```typescript
delete: RouteRegistrar;
delete: RouteRegistrar<'delete'>;
```

View file

@ -9,5 +9,5 @@ Register a route handler for `GET` request.
<b>Signature:</b>
```typescript
get: RouteRegistrar;
get: RouteRegistrar<'get'>;
```

View file

@ -16,10 +16,11 @@ export interface IRouter
| Property | Type | Description |
| --- | --- | --- |
| [delete](./kibana-plugin-server.irouter.delete.md) | <code>RouteRegistrar</code> | Register a route handler for <code>DELETE</code> request. |
| [get](./kibana-plugin-server.irouter.get.md) | <code>RouteRegistrar</code> | Register a route handler for <code>GET</code> request. |
| [delete](./kibana-plugin-server.irouter.delete.md) | <code>RouteRegistrar&lt;'delete'&gt;</code> | Register a route handler for <code>DELETE</code> request. |
| [get](./kibana-plugin-server.irouter.get.md) | <code>RouteRegistrar&lt;'get'&gt;</code> | Register a route handler for <code>GET</code> request. |
| [handleLegacyErrors](./kibana-plugin-server.irouter.handlelegacyerrors.md) | <code>&lt;P extends ObjectType, Q extends ObjectType, B extends ObjectType&gt;(handler: RequestHandler&lt;P, Q, B&gt;) =&gt; RequestHandler&lt;P, Q, B&gt;</code> | Wrap a router handler to catch and converts legacy boom errors to proper custom errors. |
| [post](./kibana-plugin-server.irouter.post.md) | <code>RouteRegistrar</code> | Register a route handler for <code>POST</code> request. |
| [put](./kibana-plugin-server.irouter.put.md) | <code>RouteRegistrar</code> | Register a route handler for <code>PUT</code> request. |
| [patch](./kibana-plugin-server.irouter.patch.md) | <code>RouteRegistrar&lt;'patch'&gt;</code> | Register a route handler for <code>PATCH</code> request. |
| [post](./kibana-plugin-server.irouter.post.md) | <code>RouteRegistrar&lt;'post'&gt;</code> | Register a route handler for <code>POST</code> request. |
| [put](./kibana-plugin-server.irouter.put.md) | <code>RouteRegistrar&lt;'put'&gt;</code> | Register a route handler for <code>PUT</code> request. |
| [routerPath](./kibana-plugin-server.irouter.routerpath.md) | <code>string</code> | Resulted path |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [IRouter](./kibana-plugin-server.irouter.md) &gt; [patch](./kibana-plugin-server.irouter.patch.md)
## IRouter.patch property
Register a route handler for `PATCH` request.
<b>Signature:</b>
```typescript
patch: RouteRegistrar<'patch'>;
```

View file

@ -9,5 +9,5 @@ Register a route handler for `POST` request.
<b>Signature:</b>
```typescript
post: RouteRegistrar;
post: RouteRegistrar<'post'>;
```

View file

@ -9,5 +9,5 @@ Register a route handler for `PUT` request.
<b>Signature:</b>
```typescript
put: RouteRegistrar;
put: RouteRegistrar<'put'>;
```

View file

@ -9,7 +9,7 @@ Kibana specific abstraction for an incoming request.
<b>Signature:</b>
```typescript
export declare class KibanaRequest<Params = unknown, Query = unknown, Body = unknown>
export declare class KibanaRequest<Params = unknown, Query = unknown, Body = unknown, Method extends RouteMethod = any>
```
## Constructors
@ -26,7 +26,7 @@ export declare class KibanaRequest<Params = unknown, Query = unknown, Body = unk
| [headers](./kibana-plugin-server.kibanarequest.headers.md) | | <code>Headers</code> | Readonly copy of incoming request headers. |
| [params](./kibana-plugin-server.kibanarequest.params.md) | | <code>Params</code> | |
| [query](./kibana-plugin-server.kibanarequest.query.md) | | <code>Query</code> | |
| [route](./kibana-plugin-server.kibanarequest.route.md) | | <code>RecursiveReadonly&lt;KibanaRequestRoute&gt;</code> | matched route details |
| [route](./kibana-plugin-server.kibanarequest.route.md) | | <code>RecursiveReadonly&lt;KibanaRequestRoute&lt;Method&gt;&gt;</code> | matched route details |
| [socket](./kibana-plugin-server.kibanarequest.socket.md) | | <code>IKibanaSocket</code> | |
| [url](./kibana-plugin-server.kibanarequest.url.md) | | <code>Url</code> | a WHATWG URL standard object. |

View file

@ -9,5 +9,5 @@ matched route details
<b>Signature:</b>
```typescript
readonly route: RecursiveReadonly<KibanaRequestRoute>;
readonly route: RecursiveReadonly<KibanaRequestRoute<Method>>;
```

View file

@ -9,14 +9,14 @@ Request specific route information exposed to a handler.
<b>Signature:</b>
```typescript
export interface KibanaRequestRoute
export interface KibanaRequestRoute<Method extends RouteMethod>
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [method](./kibana-plugin-server.kibanarequestroute.method.md) | <code>RouteMethod &#124; 'patch' &#124; 'options'</code> | |
| [options](./kibana-plugin-server.kibanarequestroute.options.md) | <code>Required&lt;RouteConfigOptions&gt;</code> | |
| [method](./kibana-plugin-server.kibanarequestroute.method.md) | <code>Method</code> | |
| [options](./kibana-plugin-server.kibanarequestroute.options.md) | <code>KibanaRequestRouteOptions&lt;Method&gt;</code> | |
| [path](./kibana-plugin-server.kibanarequestroute.path.md) | <code>string</code> | |

View file

@ -7,5 +7,5 @@
<b>Signature:</b>
```typescript
method: RouteMethod | 'patch' | 'options';
method: Method;
```

View file

@ -7,5 +7,5 @@
<b>Signature:</b>
```typescript
options: Required<RouteConfigOptions>;
options: KibanaRequestRouteOptions<Method>;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [KibanaRequestRouteOptions](./kibana-plugin-server.kibanarequestrouteoptions.md)
## KibanaRequestRouteOptions type
Route options: If 'GET' or 'OPTIONS' method, body options won't be returned.
<b>Signature:</b>
```typescript
export declare type KibanaRequestRouteOptions<Method extends RouteMethod> = Method extends 'get' | 'options' ? Required<Omit<RouteConfigOptions<Method>, 'body'>> : Required<RouteConfigOptions<Method>>;
```

View file

@ -83,6 +83,8 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [RequestHandlerContext](./kibana-plugin-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.<!-- -->Provides the following clients: - [savedObjects.client](./kibana-plugin-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [elasticsearch.dataClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request |
| [RouteConfig](./kibana-plugin-server.routeconfig.md) | Route specific configuration. |
| [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md) | Additional route options. |
| [RouteConfigOptionsBody](./kibana-plugin-server.routeconfigoptionsbody.md) | Additional body options for a route |
| [RouteSchemas](./kibana-plugin-server.routeschemas.md) | RouteSchemas contains the schemas for validating the different parts of a request. |
| [SavedObject](./kibana-plugin-server.savedobject.md) | |
| [SavedObjectAttributes](./kibana-plugin-server.savedobjectattributes.md) | The data for a Saved Object is stored as an object in the <code>attributes</code> property. |
| [SavedObjectReference](./kibana-plugin-server.savedobjectreference.md) | A reference to another saved object. |
@ -128,6 +130,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| Variable | Description |
| --- | --- |
| [kibanaResponseFactory](./kibana-plugin-server.kibanaresponsefactory.md) | Set of helpers used to create <code>KibanaResponse</code> to form HTTP response on an incoming request. Should be returned as a result of [RequestHandler](./kibana-plugin-server.requesthandler.md) execution. |
| [validBodyOutput](./kibana-plugin-server.validbodyoutput.md) | The set of valid body.output |
## Type Aliases
@ -150,6 +153,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [IContextProvider](./kibana-plugin-server.icontextprovider.md) | A function that returns a context value for a specific key of given context type. |
| [IsAuthenticated](./kibana-plugin-server.isauthenticated.md) | Return authentication status for a request. |
| [IScopedClusterClient](./kibana-plugin-server.iscopedclusterclient.md) | Serves the same purpose as "normal" <code>ClusterClient</code> but exposes additional <code>callAsCurrentUser</code> method that doesn't use credentials of the Kibana internal user (as <code>callAsInternalUser</code> does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API.<!-- -->See [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md)<!-- -->. |
| [KibanaRequestRouteOptions](./kibana-plugin-server.kibanarequestrouteoptions.md) | Route options: If 'GET' or 'OPTIONS' method, body options won't be returned. |
| [KibanaResponseFactory](./kibana-plugin-server.kibanaresponsefactory.md) | Creates an object containing request response payload, HTTP headers, error details, and other data transmitted to the client. |
| [KnownHeaders](./kibana-plugin-server.knownheaders.md) | Set of well-known HTTP headers. |
| [LifecycleResponseFactory](./kibana-plugin-server.lifecycleresponsefactory.md) | Creates an object containing redirection or error response with error details, HTTP headers, and other data transmitted to the client. |
@ -170,8 +174,9 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [ResponseError](./kibana-plugin-server.responseerror.md) | Error message and optional data send to the client in case of error. |
| [ResponseErrorAttributes](./kibana-plugin-server.responseerrorattributes.md) | Additional data to provide error details. |
| [ResponseHeaders](./kibana-plugin-server.responseheaders.md) | Http response headers to set. |
| [RouteContentType](./kibana-plugin-server.routecontenttype.md) | The set of supported parseable Content-Types |
| [RouteMethod](./kibana-plugin-server.routemethod.md) | The set of common HTTP methods supported by Kibana routing. |
| [RouteRegistrar](./kibana-plugin-server.routeregistrar.md) | Handler to declare a route. |
| [RouteRegistrar](./kibana-plugin-server.routeregistrar.md) | Route handler common definition |
| [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) | Type definition for a Saved Object attribute value |
| [SavedObjectAttributeSingle](./kibana-plugin-server.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) |
| [SavedObjectsClientContract](./kibana-plugin-server.savedobjectsclientcontract.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state.<!-- -->\#\# SavedObjectsClient errors<!-- -->Since 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 <code>isXYZError()</code> helpers exposed at <code>SavedObjectsErrorHelpers</code> should be used to understand and manage error responses from the <code>SavedObjectsClient</code>.<!-- -->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 <code>error.body.error.type</code> or doing substring checks on <code>error.body.error.reason</code>, 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 index<!-- -->From 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 index<!-- -->Unlike 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 <code>action.auto_create_index</code> 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 [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) See [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) |

View file

@ -9,7 +9,7 @@ A function executed when route path matched requested resource path. Request han
<b>Signature:</b>
```typescript
export declare type RequestHandler<P extends ObjectType, Q extends ObjectType, B extends ObjectType> = (context: RequestHandlerContext, request: KibanaRequest<TypeOf<P>, TypeOf<Q>, TypeOf<B>>, response: KibanaResponseFactory) => IKibanaResponse<any> | Promise<IKibanaResponse<any>>;
export declare type RequestHandler<P extends ObjectType, Q extends ObjectType, B extends ObjectType | Type<Buffer> | Type<Stream>, Method extends RouteMethod = any> = (context: RequestHandlerContext, request: KibanaRequest<TypeOf<P>, TypeOf<Q>, TypeOf<B>, Method>, response: KibanaResponseFactory) => IKibanaResponse<any> | Promise<IKibanaResponse<any>>;
```
## Example

View file

@ -9,14 +9,14 @@ Route specific configuration.
<b>Signature:</b>
```typescript
export interface RouteConfig<P extends ObjectType, Q extends ObjectType, B extends ObjectType>
export interface RouteConfig<P extends ObjectType, Q extends ObjectType, B extends ObjectType | Type<Buffer> | Type<Stream>, Method extends RouteMethod>
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [options](./kibana-plugin-server.routeconfig.options.md) | <code>RouteConfigOptions</code> | Additional route options [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md)<!-- -->. |
| [options](./kibana-plugin-server.routeconfig.options.md) | <code>RouteConfigOptions&lt;Method&gt;</code> | Additional route options [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md)<!-- -->. |
| [path](./kibana-plugin-server.routeconfig.path.md) | <code>string</code> | The endpoint \_within\_ the router path to register the route. |
| [validate](./kibana-plugin-server.routeconfig.validate.md) | <code>RouteSchemas&lt;P, Q, B&gt; &#124; false</code> | A schema created with <code>@kbn/config-schema</code> that every request will be validated against. |

View file

@ -9,5 +9,5 @@ Additional route options [RouteConfigOptions](./kibana-plugin-server.routeconfig
<b>Signature:</b>
```typescript
options?: RouteConfigOptions;
options?: RouteConfigOptions<Method>;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md) &gt; [body](./kibana-plugin-server.routeconfigoptions.body.md)
## RouteConfigOptions.body property
Additional body options [RouteConfigOptionsBody](./kibana-plugin-server.routeconfigoptionsbody.md)<!-- -->.
<b>Signature:</b>
```typescript
body?: Method extends 'get' | 'options' ? undefined : RouteConfigOptionsBody;
```

View file

@ -9,7 +9,7 @@ Additional route options.
<b>Signature:</b>
```typescript
export interface RouteConfigOptions
export interface RouteConfigOptions<Method extends RouteMethod>
```
## Properties
@ -17,5 +17,6 @@ export interface RouteConfigOptions
| Property | Type | Description |
| --- | --- | --- |
| [authRequired](./kibana-plugin-server.routeconfigoptions.authrequired.md) | <code>boolean</code> | A flag shows that authentication for a route: <code>enabled</code> when true <code>disabled</code> when false<!-- -->Enabled by default. |
| [body](./kibana-plugin-server.routeconfigoptions.body.md) | <code>Method extends 'get' &#124; 'options' ? undefined : RouteConfigOptionsBody</code> | Additional body options [RouteConfigOptionsBody](./kibana-plugin-server.routeconfigoptionsbody.md)<!-- -->. |
| [tags](./kibana-plugin-server.routeconfigoptions.tags.md) | <code>readonly string[]</code> | Additional metadata tag strings to attach to the route. |

View file

@ -0,0 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [RouteConfigOptionsBody](./kibana-plugin-server.routeconfigoptionsbody.md) &gt; [accepts](./kibana-plugin-server.routeconfigoptionsbody.accepts.md)
## RouteConfigOptionsBody.accepts property
A string or an array of strings with the allowed mime types for the endpoint. Use this settings to limit the set of allowed mime types. Note that allowing additional mime types not listed above will not enable them to be parsed, and if parse is true, the request will result in an error response.
Default value: allows parsing of the following mime types: \* application/json \* application/\*+json \* application/octet-stream \* application/x-www-form-urlencoded \* multipart/form-data \* text/\*
<b>Signature:</b>
```typescript
accepts?: RouteContentType | RouteContentType[] | string | string[];
```

View file

@ -0,0 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [RouteConfigOptionsBody](./kibana-plugin-server.routeconfigoptionsbody.md) &gt; [maxBytes](./kibana-plugin-server.routeconfigoptionsbody.maxbytes.md)
## RouteConfigOptionsBody.maxBytes property
Limits the size of incoming payloads to the specified byte count. Allowing very large payloads may cause the server to run out of memory.
Default value: The one set in the kibana.yml config file under the parameter `server.maxPayloadBytes`<!-- -->.
<b>Signature:</b>
```typescript
maxBytes?: number;
```

View file

@ -0,0 +1,23 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [RouteConfigOptionsBody](./kibana-plugin-server.routeconfigoptionsbody.md)
## RouteConfigOptionsBody interface
Additional body options for a route
<b>Signature:</b>
```typescript
export interface RouteConfigOptionsBody
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [accepts](./kibana-plugin-server.routeconfigoptionsbody.accepts.md) | <code>RouteContentType &#124; RouteContentType[] &#124; string &#124; string[]</code> | A string or an array of strings with the allowed mime types for the endpoint. Use this settings to limit the set of allowed mime types. Note that allowing additional mime types not listed above will not enable them to be parsed, and if parse is true, the request will result in an error response.<!-- -->Default value: allows parsing of the following mime types: \* application/json \* application/\*+json \* application/octet-stream \* application/x-www-form-urlencoded \* multipart/form-data \* text/\* |
| [maxBytes](./kibana-plugin-server.routeconfigoptionsbody.maxbytes.md) | <code>number</code> | Limits the size of incoming payloads to the specified byte count. Allowing very large payloads may cause the server to run out of memory.<!-- -->Default value: The one set in the kibana.yml config file under the parameter <code>server.maxPayloadBytes</code>. |
| [output](./kibana-plugin-server.routeconfigoptionsbody.output.md) | <code>typeof validBodyOutput[number]</code> | The processed payload format. The value must be one of: \* 'data' - the incoming payload is read fully into memory. If parse is true, the payload is parsed (JSON, form-decoded, multipart) based on the 'Content-Type' header. If parse is false, a raw Buffer is returned. \* 'stream' - the incoming payload is made available via a Stream.Readable interface. If the payload is 'multipart/form-data' and parse is true, field values are presented as text while files are provided as streams. File streams from a 'multipart/form-data' upload will also have a hapi property containing the filename and headers properties. Note that payload streams for multipart payloads are a synthetic interface created on top of the entire multipart content loaded into memory. To avoid loading large multipart payloads into memory, set parse to false and handle the multipart payload in the handler using a streaming parser (e.g. pez).<!-- -->Default value: 'data', unless no validation.body is provided in the route definition. In that case the default is 'stream' to alleviate memory pressure. |
| [parse](./kibana-plugin-server.routeconfigoptionsbody.parse.md) | <code>boolean &#124; 'gunzip'</code> | Determines if the incoming payload is processed or presented raw. Available values: \* true - if the request 'Content-Type' matches the allowed mime types set by allow (for the whole payload as well as parts), the payload is converted into an object when possible. If the format is unknown, a Bad Request (400) error response is sent. Any known content encoding is decoded. \* false - the raw payload is returned unmodified. \* 'gunzip' - the raw payload is returned unmodified after any known content encoding is decoded.<!-- -->Default value: true, unless no validation.body is provided in the route definition. In that case the default is false to alleviate memory pressure. |

View file

@ -0,0 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [RouteConfigOptionsBody](./kibana-plugin-server.routeconfigoptionsbody.md) &gt; [output](./kibana-plugin-server.routeconfigoptionsbody.output.md)
## RouteConfigOptionsBody.output property
The processed payload format. The value must be one of: \* 'data' - the incoming payload is read fully into memory. If parse is true, the payload is parsed (JSON, form-decoded, multipart) based on the 'Content-Type' header. If parse is false, a raw Buffer is returned. \* 'stream' - the incoming payload is made available via a Stream.Readable interface. If the payload is 'multipart/form-data' and parse is true, field values are presented as text while files are provided as streams. File streams from a 'multipart/form-data' upload will also have a hapi property containing the filename and headers properties. Note that payload streams for multipart payloads are a synthetic interface created on top of the entire multipart content loaded into memory. To avoid loading large multipart payloads into memory, set parse to false and handle the multipart payload in the handler using a streaming parser (e.g. pez).
Default value: 'data', unless no validation.body is provided in the route definition. In that case the default is 'stream' to alleviate memory pressure.
<b>Signature:</b>
```typescript
output?: typeof validBodyOutput[number];
```

View file

@ -0,0 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [RouteConfigOptionsBody](./kibana-plugin-server.routeconfigoptionsbody.md) &gt; [parse](./kibana-plugin-server.routeconfigoptionsbody.parse.md)
## RouteConfigOptionsBody.parse property
Determines if the incoming payload is processed or presented raw. Available values: \* true - if the request 'Content-Type' matches the allowed mime types set by allow (for the whole payload as well as parts), the payload is converted into an object when possible. If the format is unknown, a Bad Request (400) error response is sent. Any known content encoding is decoded. \* false - the raw payload is returned unmodified. \* 'gunzip' - the raw payload is returned unmodified after any known content encoding is decoded.
Default value: true, unless no validation.body is provided in the route definition. In that case the default is false to alleviate memory pressure.
<b>Signature:</b>
```typescript
parse?: boolean | 'gunzip';
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [RouteContentType](./kibana-plugin-server.routecontenttype.md)
## RouteContentType type
The set of supported parseable Content-Types
<b>Signature:</b>
```typescript
export declare type RouteContentType = 'application/json' | 'application/*+json' | 'application/octet-stream' | 'application/x-www-form-urlencoded' | 'multipart/form-data' | 'text/*';
```

View file

@ -9,5 +9,5 @@ The set of common HTTP methods supported by Kibana routing.
<b>Signature:</b>
```typescript
export declare type RouteMethod = 'get' | 'post' | 'put' | 'delete';
export declare type RouteMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options';
```

View file

@ -4,10 +4,10 @@
## RouteRegistrar type
Handler to declare a route.
Route handler common definition
<b>Signature:</b>
```typescript
export declare type RouteRegistrar = <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>) => void;
export declare type RouteRegistrar<Method extends RouteMethod> = <P extends ObjectType, Q extends ObjectType, B extends ObjectType | Type<Buffer> | Type<Stream>>(route: RouteConfig<P, Q, B, Method>, handler: RequestHandler<P, Q, B, Method>) => void;
```

View file

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

View file

@ -0,0 +1,22 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [RouteSchemas](./kibana-plugin-server.routeschemas.md)
## RouteSchemas interface
RouteSchemas contains the schemas for validating the different parts of a request.
<b>Signature:</b>
```typescript
export interface RouteSchemas<P extends ObjectType, Q extends ObjectType, B extends ObjectType | Type<Buffer> | Type<Stream>>
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [body](./kibana-plugin-server.routeschemas.body.md) | <code>B</code> | |
| [params](./kibana-plugin-server.routeschemas.params.md) | <code>P</code> | |
| [query](./kibana-plugin-server.routeschemas.query.md) | <code>Q</code> | |

View file

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

View file

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

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [validBodyOutput](./kibana-plugin-server.validbodyoutput.md)
## validBodyOutput variable
The set of valid body.output
<b>Signature:</b>
```typescript
validBodyOutput: readonly ["data", "stream"]
```

View file

@ -12,6 +12,8 @@ Kibana configuration entries providing developers with a fully typed model of th
- [`schema.number()`](#schemanumber)
- [`schema.boolean()`](#schemaboolean)
- [`schema.literal()`](#schemaliteral)
- [`schema.buffer()`](#schemabuffer)
- [`schema.stream()`](#schemastream)
- [Composite types](#composite-types)
- [`schema.arrayOf()`](#schemaarrayof)
- [`schema.object()`](#schemaobject)
@ -173,6 +175,36 @@ const valueSchema = [
];
```
#### `schema.buffer()`
Validates input data as a NodeJS `Buffer`.
__Output type:__ `Buffer`
__Options:__
* `defaultValue: TBuffer | Reference<TBuffer> | (() => TBuffer)` - defines a default value, see [Default values](#default-values) section for more details.
* `validate: (value: TBuffer) => Buffer | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details.
__Usage:__
```typescript
const valueSchema = schema.buffer({ defaultValue: Buffer.from('Hi, there!') });
```
#### `schema.stream()`
Validates input data as a NodeJS `stream`.
__Output type:__ `Stream`, `Readable` or `Writtable`
__Options:__
* `defaultValue: TStream | Reference<TStream> | (() => TStream)` - defines a default value, see [Default values](#default-values) section for more details.
* `validate: (value: TStream) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details.
__Usage:__
```typescript
const valueSchema = schema.stream({ defaultValue: new Stream() });
```
### Composite types
#### `schema.arrayOf()`

View file

@ -18,6 +18,7 @@
*/
import { Duration } from 'moment';
import { Stream } from 'stream';
import { ByteSizeValue } from './byte_size_value';
import { ContextReference, Reference, SiblingReference } from './references';
@ -26,6 +27,7 @@ import {
ArrayOptions,
ArrayType,
BooleanType,
BufferType,
ByteSizeOptions,
ByteSizeType,
ConditionalType,
@ -52,6 +54,7 @@ import {
UnionType,
URIOptions,
URIType,
StreamType,
} from './types';
export { ObjectType, TypeOf, Type };
@ -65,6 +68,14 @@ function boolean(options?: TypeOptions<boolean>): Type<boolean> {
return new BooleanType(options);
}
function buffer(options?: TypeOptions<Buffer>): Type<Buffer> {
return new BufferType(options);
}
function stream(options?: TypeOptions<Stream>): Type<Stream> {
return new StreamType(options);
}
function string(options?: StringOptions): Type<string> {
return new StringType(options);
}
@ -188,6 +199,7 @@ export const schema = {
any,
arrayOf,
boolean,
buffer,
byteSize,
conditional,
contextRef,
@ -201,6 +213,7 @@ export const schema = {
object,
oneOf,
recordOf,
stream,
siblingRef,
string,
uri,

View file

@ -29,6 +29,7 @@ import {
} from 'joi';
import { isPlainObject } from 'lodash';
import { isDuration } from 'moment';
import { Stream } from 'stream';
import { ByteSizeValue, ensureByteSizeValue } from '../byte_size_value';
import { ensureDuration } from '../duration';
@ -89,6 +90,33 @@ export const internals = Joi.extend([
},
rules: [anyCustomRule],
},
{
name: 'binary',
base: Joi.binary(),
coerce(value: any, state: State, options: ValidationOptions) {
// If value isn't defined, let Joi handle default value if it's defined.
if (value !== undefined && !(typeof value === 'object' && Buffer.isBuffer(value))) {
return this.createError('binary.base', { value }, state, options);
}
return value;
},
rules: [anyCustomRule],
},
{
name: 'stream',
pre(value: any, state: State, options: ValidationOptions) {
// If value isn't defined, let Joi handle default value if it's defined.
if (value instanceof Stream) {
return value as any;
}
return this.createError('stream.base', { value }, state, options);
},
rules: [anyCustomRule],
},
{
name: 'string',

View file

@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`includes namespace in failure 1`] = `"[foo-namespace]: expected value of type [Buffer] but got [undefined]"`;
exports[`is required by default 1`] = `"expected value of type [Buffer] but got [undefined]"`;
exports[`returns error when not a buffer 1`] = `"expected value of type [Buffer] but got [number]"`;
exports[`returns error when not a buffer 2`] = `"expected value of type [Buffer] but got [Array]"`;
exports[`returns error when not a buffer 3`] = `"expected value of type [Buffer] but got [string]"`;

View file

@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`includes namespace in failure 1`] = `"[foo-namespace]: expected value of type [Stream] but got [undefined]"`;
exports[`is required by default 1`] = `"expected value of type [Buffer] but got [undefined]"`;
exports[`returns error when not a stream 1`] = `"expected value of type [Stream] but got [number]"`;
exports[`returns error when not a stream 2`] = `"expected value of type [Stream] but got [Array]"`;
exports[`returns error when not a stream 3`] = `"expected value of type [Stream] but got [string]"`;

View file

@ -0,0 +1,57 @@
/*
* 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 { schema } from '..';
test('returns value by default', () => {
const value = Buffer.from('Hi!');
expect(schema.buffer().validate(value)).toStrictEqual(value);
});
test('is required by default', () => {
expect(() => schema.buffer().validate(undefined)).toThrowErrorMatchingSnapshot();
});
test('includes namespace in failure', () => {
expect(() =>
schema.buffer().validate(undefined, {}, 'foo-namespace')
).toThrowErrorMatchingSnapshot();
});
describe('#defaultValue', () => {
test('returns default when undefined', () => {
const value = Buffer.from('Hi!');
expect(schema.buffer({ defaultValue: value }).validate(undefined)).toStrictEqual(value);
});
test('returns value when specified', () => {
const value = Buffer.from('Hi!');
expect(schema.buffer({ defaultValue: Buffer.from('Bye!') }).validate(value)).toStrictEqual(
value
);
});
});
test('returns error when not a buffer', () => {
expect(() => schema.buffer().validate(123)).toThrowErrorMatchingSnapshot();
expect(() => schema.buffer().validate([1, 2, 3])).toThrowErrorMatchingSnapshot();
expect(() => schema.buffer().validate('abc')).toThrowErrorMatchingSnapshot();
});

View file

@ -0,0 +1,34 @@
/*
* 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 typeDetect from 'type-detect';
import { internals } from '../internals';
import { Type, TypeOptions } from './type';
export class BufferType extends Type<Buffer> {
constructor(options?: TypeOptions<Buffer>) {
super(internals.binary(), options);
}
protected handleError(type: string, { value }: Record<string, any>) {
if (type === 'any.required' || type === 'binary.base') {
return `expected value of type [Buffer] but got [${typeDetect(value)}]`;
}
}
}

View file

@ -21,6 +21,7 @@ export { Type, TypeOptions } from './type';
export { AnyType } from './any_type';
export { ArrayOptions, ArrayType } from './array_type';
export { BooleanType } from './boolean_type';
export { BufferType } from './buffer_type';
export { ByteSizeOptions, ByteSizeType } from './byte_size_type';
export { ConditionalType, ConditionalTypeValue } from './conditional_type';
export { DurationOptions, DurationType } from './duration_type';
@ -30,6 +31,7 @@ export { MapOfOptions, MapOfType } from './map_type';
export { NumberOptions, NumberType } from './number_type';
export { ObjectType, ObjectTypeOptions, Props, TypeOf } from './object_type';
export { RecordOfOptions, RecordOfType } from './record_type';
export { StreamType } from './stream_type';
export { StringOptions, StringType } from './string_type';
export { UnionType } from './union_type';
export { URIOptions, URIType } from './uri_type';

View file

@ -0,0 +1,71 @@
/*
* 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 { schema } from '..';
import { Stream, Readable, Writable, PassThrough } from 'stream';
test('returns value by default', () => {
const value = new Stream();
expect(schema.stream().validate(value)).toStrictEqual(value);
});
test('Readable is valid', () => {
const value = new Readable();
expect(schema.stream().validate(value)).toStrictEqual(value);
});
test('Writable is valid', () => {
const value = new Writable();
expect(schema.stream().validate(value)).toStrictEqual(value);
});
test('Passthrough is valid', () => {
const value = new PassThrough();
expect(schema.stream().validate(value)).toStrictEqual(value);
});
test('is required by default', () => {
expect(() => schema.buffer().validate(undefined)).toThrowErrorMatchingSnapshot();
});
test('includes namespace in failure', () => {
expect(() =>
schema.stream().validate(undefined, {}, 'foo-namespace')
).toThrowErrorMatchingSnapshot();
});
describe('#defaultValue', () => {
test('returns default when undefined', () => {
const value = new Stream();
expect(schema.stream({ defaultValue: value }).validate(undefined)).toStrictEqual(value);
});
test('returns value when specified', () => {
const value = new Stream();
expect(schema.stream({ defaultValue: new PassThrough() }).validate(value)).toStrictEqual(value);
});
});
test('returns error when not a stream', () => {
expect(() => schema.stream().validate(123)).toThrowErrorMatchingSnapshot();
expect(() => schema.stream().validate([1, 2, 3])).toThrowErrorMatchingSnapshot();
expect(() => schema.stream().validate('abc')).toThrowErrorMatchingSnapshot();
});

View file

@ -0,0 +1,35 @@
/*
* 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 typeDetect from 'type-detect';
import { Stream } from 'stream';
import { internals } from '../internals';
import { Type, TypeOptions } from './type';
export class StreamType extends Type<Stream> {
constructor(options?: TypeOptions<Stream>) {
super(internals.stream(), options);
}
protected handleError(type: string, { value }: Record<string, any>) {
if (type === 'any.required' || type === 'stream.base') {
return `expected value of type [Stream] but got [${typeDetect(value)}]`;
}
}
}

View file

@ -38,6 +38,7 @@ declare module 'joi' {
duration: () => AnySchema;
map: () => MapSchema;
record: () => RecordSchema;
stream: () => AnySchema;
};
interface AnySchema {

View file

@ -54,7 +54,7 @@ function createKibanaRequestMock({
}: RequestFixtureOptions = {}) {
const queryString = querystring.stringify(query);
return KibanaRequest.from(
{
createRawRequestMock({
headers,
params,
query,
@ -71,13 +71,13 @@ function createKibanaRequestMock({
raw: {
req: { socket },
},
} as any,
}),
{
params: schema.object({}, { allowUnknowns: true }),
body: schema.object({}, { allowUnknowns: true }),
query: schema.object({}, { allowUnknowns: true }),
}
);
) as KibanaRequest<Readonly<{}>, Readonly<{}>, Readonly<{}>>;
}
type DeepPartial<T> = T extends any[]

View file

@ -30,6 +30,7 @@ import { HttpConfig } from './http_config';
import { Router } from './router';
import { loggingServiceMock } from '../logging/logging_service.mock';
import { HttpServer } from './http_server';
import { Readable } from 'stream';
const cookieOptions = {
name: 'sid',
@ -577,6 +578,157 @@ test('exposes route details of incoming request to a route handler', async () =>
});
});
test('exposes route details of incoming request to a route handler (POST + payload options)', async () => {
const { registerRouter, server: innerServer } = await server.setup(config);
const router = new Router('', logger, enhanceWithContext);
router.post(
{
path: '/',
validate: { body: schema.object({ test: schema.number() }) },
options: { body: { accepts: 'application/json' } },
},
(context, req, res) => res.ok({ body: req.route })
);
registerRouter(router);
await server.start();
await supertest(innerServer.listener)
.post('/')
.send({ test: 1 })
.expect(200, {
method: 'post',
path: '/',
options: {
authRequired: true,
tags: [],
body: {
parse: true, // hapi populates the default
maxBytes: 1024, // hapi populates the default
accepts: ['application/json'],
output: 'data',
},
},
});
});
describe('body options', () => {
test('should reject the request because the Content-Type in the request is not valid', async () => {
const { registerRouter, server: innerServer } = await server.setup(config);
const router = new Router('', logger, enhanceWithContext);
router.post(
{
path: '/',
validate: { body: schema.object({ test: schema.number() }) },
options: { body: { accepts: 'multipart/form-data' } }, // supertest sends 'application/json'
},
(context, req, res) => res.ok({ body: req.route })
);
registerRouter(router);
await server.start();
await supertest(innerServer.listener)
.post('/')
.send({ test: 1 })
.expect(415, {
statusCode: 415,
error: 'Unsupported Media Type',
message: 'Unsupported Media Type',
});
});
test('should reject the request because the payload is too large', async () => {
const { registerRouter, server: innerServer } = await server.setup(config);
const router = new Router('', logger, enhanceWithContext);
router.post(
{
path: '/',
validate: { body: schema.object({ test: schema.number() }) },
options: { body: { maxBytes: 1 } },
},
(context, req, res) => res.ok({ body: req.route })
);
registerRouter(router);
await server.start();
await supertest(innerServer.listener)
.post('/')
.send({ test: 1 })
.expect(413, {
statusCode: 413,
error: 'Request Entity Too Large',
message: 'Payload content length greater than maximum allowed: 1',
});
});
test('should not parse the content in the request', async () => {
const { registerRouter, server: innerServer } = await server.setup(config);
const router = new Router('', logger, enhanceWithContext);
router.post(
{
path: '/',
validate: { body: schema.buffer() },
options: { body: { parse: false } },
},
(context, req, res) => {
try {
expect(req.body).toBeInstanceOf(Buffer);
expect(req.body.toString()).toBe(JSON.stringify({ test: 1 }));
return res.ok({ body: req.route.options.body });
} catch (err) {
return res.internalError({ body: err.message });
}
}
);
registerRouter(router);
await server.start();
await supertest(innerServer.listener)
.post('/')
.send({ test: 1 })
.expect(200, {
parse: false,
maxBytes: 1024, // hapi populates the default
output: 'data',
});
});
});
test('should return a stream in the body', async () => {
const { registerRouter, server: innerServer } = await server.setup(config);
const router = new Router('', logger, enhanceWithContext);
router.put(
{
path: '/',
validate: { body: schema.stream() },
options: { body: { output: 'stream' } },
},
(context, req, res) => {
try {
expect(req.body).toBeInstanceOf(Readable);
return res.ok({ body: req.route.options.body });
} catch (err) {
return res.internalError({ body: err.message });
}
}
);
registerRouter(router);
await server.start();
await supertest(innerServer.listener)
.put('/')
.send({ test: 1 })
.expect(200, {
parse: true,
maxBytes: 1024, // hapi populates the default
output: 'stream',
});
});
describe('setup contract', () => {
describe('#createSessionStorage', () => {
it('creates session storage factory', async () => {

View file

@ -127,21 +127,26 @@ export class HttpServer {
for (const router of this.registeredRouters) {
for (const route of router.getRoutes()) {
this.log.debug(`registering route handler for [${route.path}]`);
const { authRequired = true, tags } = route.options;
// Hapi does not allow payload validation to be specified for 'head' or 'get' requests
const validate = ['head', 'get'].includes(route.method) ? undefined : { payload: true };
const { authRequired = true, tags, body = {} } = route.options;
const { accepts: allow, maxBytes, output, parse } = body;
this.server.route({
handler: route.handler,
method: route.method,
path: route.path,
options: {
auth: authRequired ? undefined : false,
// Enforcing the comparison with true because plugins could overwrite the auth strategy by doing `options: { authRequired: authStrategy as any }`
auth: authRequired === true ? undefined : false,
tags: tags ? Array.from(tags) : undefined,
// TODO: This 'validate' section can be removed once the legacy platform is completely removed.
// We are telling Hapi that NP routes can accept any payload, so that it can bypass the default
// validation applied in ./http_tools#getServerOptions
// (All NP routes are already required to specify their own validation in order to access the payload)
validate,
payload: [allow, maxBytes, output, parse].some(v => typeof v !== 'undefined')
? { allow, maxBytes, output, parse }
: undefined,
},
});
}

View file

@ -43,6 +43,7 @@ const createRouterMock = (): jest.Mocked<IRouter> => ({
get: jest.fn(),
post: jest.fn(),
put: jest.fn(),
patch: jest.fn(),
delete: jest.fn(),
getRoutes: jest.fn(),
handleLegacyErrors: jest.fn().mockImplementation(handler => handler),

View file

@ -30,6 +30,7 @@ export {
ErrorHttpResponseOptions,
KibanaRequest,
KibanaRequestRoute,
KibanaRequestRouteOptions,
IKibanaResponse,
KnownHeaders,
LegacyRequest,
@ -44,8 +45,12 @@ export {
RouteConfig,
IRouter,
RouteMethod,
RouteConfigOptions,
RouteRegistrar,
RouteConfigOptions,
RouteSchemas,
RouteConfigOptionsBody,
RouteContentType,
validBodyOutput,
} from './router';
export { BasePathProxyServer } from './base_path_proxy_server';
export { OnPreAuthHandler, OnPreAuthToolkit } from './lifecycle/on_pre_auth';

View file

@ -23,13 +23,14 @@ import { KibanaRequest } from './request';
import { KibanaResponseFactory } from './response';
import { RequestHandler } from './router';
import { RequestHandlerContext } from '../../../server';
import { RouteMethod } from './route';
export const wrapErrors = <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(
handler: RequestHandler<P, Q, B>
): RequestHandler<P, Q, B> => {
handler: RequestHandler<P, Q, B, RouteMethod>
): RequestHandler<P, Q, B, RouteMethod> => {
return async (
context: RequestHandlerContext,
request: KibanaRequest<TypeOf<P>, TypeOf<Q>, TypeOf<B>>,
request: KibanaRequest<TypeOf<P>, TypeOf<Q>, TypeOf<B>, RouteMethod>,
response: KibanaResponseFactory
) => {
try {

View file

@ -22,11 +22,20 @@ export { Router, RequestHandler, IRouter, RouteRegistrar } from './router';
export {
KibanaRequest,
KibanaRequestRoute,
KibanaRequestRouteOptions,
isRealRequest,
LegacyRequest,
ensureRawRequest,
} from './request';
export { RouteMethod, RouteConfig, RouteConfigOptions } from './route';
export {
RouteMethod,
RouteConfig,
RouteConfigOptions,
RouteSchemas,
RouteContentType,
RouteConfigOptionsBody,
validBodyOutput,
} from './route';
export { HapiResponseAdapter } from './response_adapter';
export {
CustomHttpResponseOptions,

View file

@ -20,23 +20,32 @@
import { Url } from 'url';
import { Request } from 'hapi';
import { ObjectType, TypeOf } from '@kbn/config-schema';
import { ObjectType, Type, TypeOf } from '@kbn/config-schema';
import { Stream } from 'stream';
import { deepFreeze, RecursiveReadonly } from '../../../utils';
import { Headers } from './headers';
import { RouteMethod, RouteSchemas, RouteConfigOptions } from './route';
import { RouteMethod, RouteSchemas, RouteConfigOptions, validBodyOutput } from './route';
import { KibanaSocket, IKibanaSocket } from './socket';
const requestSymbol = Symbol('request');
/**
* Route options: If 'GET' or 'OPTIONS' method, body options won't be returned.
* @public
*/
export type KibanaRequestRouteOptions<Method extends RouteMethod> = Method extends 'get' | 'options'
? Required<Omit<RouteConfigOptions<Method>, 'body'>>
: Required<RouteConfigOptions<Method>>;
/**
* Request specific route information exposed to a handler.
* @public
* */
export interface KibanaRequestRoute {
export interface KibanaRequestRoute<Method extends RouteMethod> {
path: string;
method: RouteMethod | 'patch' | 'options';
options: Required<RouteConfigOptions>;
method: Method;
options: KibanaRequestRouteOptions<Method>;
}
/**
@ -50,17 +59,22 @@ export interface LegacyRequest extends Request {} // eslint-disable-line @typesc
* Kibana specific abstraction for an incoming request.
* @public
*/
export class KibanaRequest<Params = unknown, Query = unknown, Body = unknown> {
export class KibanaRequest<
Params = unknown,
Query = unknown,
Body = unknown,
Method extends RouteMethod = any
> {
/**
* Factory for creating requests. Validates the request before creating an
* instance of a KibanaRequest.
* @internal
*/
public static from<P extends ObjectType, Q extends ObjectType, B extends ObjectType>(
req: Request,
routeSchemas?: RouteSchemas<P, Q, B>,
withoutSecretHeaders: boolean = true
) {
public static from<
P extends ObjectType,
Q extends ObjectType,
B extends ObjectType | Type<Buffer> | Type<Stream>
>(req: Request, routeSchemas?: RouteSchemas<P, Q, B>, withoutSecretHeaders: boolean = true) {
const requestParts = KibanaRequest.validate(req, routeSchemas);
return new KibanaRequest(
req,
@ -77,7 +91,11 @@ export class KibanaRequest<Params = unknown, Query = unknown, Body = unknown> {
* received in the route handler.
* @internal
*/
private static validate<P extends ObjectType, Q extends ObjectType, B extends ObjectType>(
private static validate<
P extends ObjectType,
Q extends ObjectType,
B extends ObjectType | Type<Buffer> | Type<Stream>
>(
req: Request,
routeSchemas: RouteSchemas<P, Q, B> | undefined
): {
@ -113,7 +131,7 @@ export class KibanaRequest<Params = unknown, Query = unknown, Body = unknown> {
/** a WHATWG URL standard object. */
public readonly url: Url;
/** matched route details */
public readonly route: RecursiveReadonly<KibanaRequestRoute>;
public readonly route: RecursiveReadonly<KibanaRequestRoute<Method>>;
/**
* Readonly copy of incoming request headers.
* @remarks
@ -148,15 +166,28 @@ export class KibanaRequest<Params = unknown, Query = unknown, Body = unknown> {
this.socket = new KibanaSocket(request.raw.req.socket);
}
private getRouteInfo() {
private getRouteInfo(): KibanaRequestRoute<Method> {
const request = this[requestSymbol];
const method = request.method as Method;
const { parse, maxBytes, allow, output } = request.route.settings.payload || {};
const options = ({
authRequired: request.route.settings.auth !== false,
tags: request.route.settings.tags || [],
body: ['get', 'options'].includes(method)
? undefined
: {
parse,
maxBytes,
accepts: allow,
output: output as typeof validBodyOutput[number], // We do not support all the HAPI-supported outputs and TS complains
},
} as unknown) as KibanaRequestRouteOptions<Method>; // TS does not understand this is OK so I'm enforced to do this enforced casting
return {
path: request.path,
method: request.method,
options: {
authRequired: request.route.settings.auth !== false,
tags: request.route.settings.tags || [],
},
method,
options,
};
}
}

View file

@ -17,18 +17,89 @@
* under the License.
*/
import { ObjectType } from '@kbn/config-schema';
import { ObjectType, Type } from '@kbn/config-schema';
import { Stream } from 'stream';
/**
* The set of common HTTP methods supported by Kibana routing.
* @public
*/
export type RouteMethod = 'get' | 'post' | 'put' | 'delete';
export type RouteMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options';
/**
* The set of valid body.output
* @public
*/
export const validBodyOutput = ['data', 'stream'] as const;
/**
* The set of supported parseable Content-Types
* @public
*/
export type RouteContentType =
| 'application/json'
| 'application/*+json'
| 'application/octet-stream'
| 'application/x-www-form-urlencoded'
| 'multipart/form-data'
| 'text/*';
/**
* Additional body options for a route
* @public
*/
export interface RouteConfigOptionsBody {
/**
* A string or an array of strings with the allowed mime types for the endpoint. Use this settings to limit the set of allowed mime types. Note that allowing additional mime types not listed
* above will not enable them to be parsed, and if parse is true, the request will result in an error response.
*
* Default value: allows parsing of the following mime types:
* * application/json
* * application/*+json
* * application/octet-stream
* * application/x-www-form-urlencoded
* * multipart/form-data
* * text/*
*/
accepts?: RouteContentType | RouteContentType[] | string | string[];
/**
* Limits the size of incoming payloads to the specified byte count. Allowing very large payloads may cause the server to run out of memory.
*
* Default value: The one set in the kibana.yml config file under the parameter `server.maxPayloadBytes`.
*/
maxBytes?: number;
/**
* The processed payload format. The value must be one of:
* * 'data' - the incoming payload is read fully into memory. If parse is true, the payload is parsed (JSON, form-decoded, multipart) based on the 'Content-Type' header. If parse is false, a raw
* Buffer is returned.
* * 'stream' - the incoming payload is made available via a Stream.Readable interface. If the payload is 'multipart/form-data' and parse is true, field values are presented as text while files
* are provided as streams. File streams from a 'multipart/form-data' upload will also have a hapi property containing the filename and headers properties. Note that payload streams for multipart
* payloads are a synthetic interface created on top of the entire multipart content loaded into memory. To avoid loading large multipart payloads into memory, set parse to false and handle the
* multipart payload in the handler using a streaming parser (e.g. pez).
*
* Default value: 'data', unless no validation.body is provided in the route definition. In that case the default is 'stream' to alleviate memory pressure.
*/
output?: typeof validBodyOutput[number];
/**
* Determines if the incoming payload is processed or presented raw. Available values:
* * true - if the request 'Content-Type' matches the allowed mime types set by allow (for the whole payload as well as parts), the payload is converted into an object when possible. If the
* format is unknown, a Bad Request (400) error response is sent. Any known content encoding is decoded.
* * false - the raw payload is returned unmodified.
* * 'gunzip' - the raw payload is returned unmodified after any known content encoding is decoded.
*
* Default value: true, unless no validation.body is provided in the route definition. In that case the default is false to alleviate memory pressure.
*/
parse?: boolean | 'gunzip';
}
/**
* Additional route options.
* @public
*/
export interface RouteConfigOptions {
export interface RouteConfigOptions<Method extends RouteMethod> {
/**
* A flag shows that authentication for a route:
* `enabled` when true
@ -42,13 +113,23 @@ export interface RouteConfigOptions {
* Additional metadata tag strings to attach to the route.
*/
tags?: readonly string[];
/**
* Additional body options {@link RouteConfigOptionsBody}.
*/
body?: Method extends 'get' | 'options' ? undefined : RouteConfigOptionsBody;
}
/**
* Route specific configuration.
* @public
*/
export interface RouteConfig<P extends ObjectType, Q extends ObjectType, B extends ObjectType> {
export interface RouteConfig<
P extends ObjectType,
Q extends ObjectType,
B extends ObjectType | Type<Buffer> | Type<Stream>,
Method extends RouteMethod
> {
/**
* The endpoint _within_ the router path to register the route.
*
@ -125,7 +206,7 @@ export interface RouteConfig<P extends ObjectType, Q extends ObjectType, B exten
/**
* Additional route options {@link RouteConfigOptions}.
*/
options?: RouteConfigOptions;
options?: RouteConfigOptions<Method>;
}
/**
@ -133,7 +214,11 @@ export interface RouteConfig<P extends ObjectType, Q extends ObjectType, B exten
* request.
* @public
*/
export interface RouteSchemas<P extends ObjectType, Q extends ObjectType, B extends ObjectType> {
export interface RouteSchemas<
P extends ObjectType,
Q extends ObjectType,
B extends ObjectType | Type<Buffer> | Type<Stream>
> {
params?: P;
query?: Q;
body?: B;

View file

@ -19,6 +19,7 @@
import { Router } from './router';
import { loggingServiceMock } from '../../logging/logging_service.mock';
import { schema } from '@kbn/config-schema';
const logger = loggingServiceMock.create().get();
const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {});
@ -45,5 +46,46 @@ describe('Router', () => {
`"Expected a valid schema declared with '@kbn/config-schema' package at key: [params]."`
);
});
it('throws if options.body.output is not a valid value', () => {
const router = new Router('', logger, enhanceWithContext);
expect(() =>
router.post(
// we use 'any' because TS already checks we cannot provide this body.output
{
path: '/',
options: { body: { output: 'file' } } as any, // We explicitly don't support 'file'
validate: { body: schema.object({}, { allowUnknowns: true }) },
},
(context, req, res) => res.ok({})
)
).toThrowErrorMatchingInlineSnapshot(
`"[options.body.output: 'file'] in route POST / is not valid. Only 'data' or 'stream' are valid."`
);
});
it('should default `output: "stream" and parse: false` when no body validation is required but not a GET', () => {
const router = new Router('', logger, enhanceWithContext);
router.post({ path: '/', validate: {} }, (context, req, res) => res.ok({}));
const [route] = router.getRoutes();
expect(route.options).toEqual({ body: { output: 'stream', parse: false } });
});
it('should NOT default `output: "stream" and parse: false` when the user has specified body options (he cares about it)', () => {
const router = new Router('', logger, enhanceWithContext);
router.post(
{ path: '/', options: { body: { maxBytes: 1 } }, validate: {} },
(context, req, res) => res.ok({})
);
const [route] = router.getRoutes();
expect(route.options).toEqual({ body: { maxBytes: 1 } });
});
it('should NOT default `output: "stream" and parse: false` when no body validation is required and GET', () => {
const router = new Router('', logger, enhanceWithContext);
router.get({ path: '/', validate: {} }, (context, req, res) => res.ok({}));
const [route] = router.getRoutes();
expect(route.options).toEqual({});
});
});
});

View file

@ -21,10 +21,17 @@ import { ObjectType, TypeOf, Type } from '@kbn/config-schema';
import { Request, ResponseObject, ResponseToolkit } from 'hapi';
import Boom from 'boom';
import { Stream } from 'stream';
import { Logger } from '../../logging';
import { KibanaRequest } from './request';
import { KibanaResponseFactory, kibanaResponseFactory, IKibanaResponse } from './response';
import { RouteConfig, RouteConfigOptions, RouteMethod, RouteSchemas } from './route';
import {
RouteConfig,
RouteConfigOptions,
RouteMethod,
RouteSchemas,
validBodyOutput,
} from './route';
import { HapiResponseAdapter } from './response_adapter';
import { RequestHandlerContext } from '../../../server';
import { wrapErrors } from './error_wrapper';
@ -32,17 +39,22 @@ import { wrapErrors } from './error_wrapper';
interface RouterRoute {
method: RouteMethod;
path: string;
options: RouteConfigOptions;
options: RouteConfigOptions<RouteMethod>;
handler: (req: Request, responseToolkit: ResponseToolkit) => Promise<ResponseObject | Boom<any>>;
}
/**
* Handler to declare a route.
* Route handler common definition
*
* @public
*/
export type RouteRegistrar = <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(
route: RouteConfig<P, Q, B>,
handler: RequestHandler<P, Q, B>
export type RouteRegistrar<Method extends RouteMethod> = <
P extends ObjectType,
Q extends ObjectType,
B extends ObjectType | Type<Buffer> | Type<Stream>
>(
route: RouteConfig<P, Q, B, Method>,
handler: RequestHandler<P, Q, B, Method>
) => void;
/**
@ -62,28 +74,35 @@ export interface IRouter {
* @param route {@link RouteConfig} - a route configuration.
* @param handler {@link RequestHandler} - a function to call to respond to an incoming request
*/
get: RouteRegistrar;
get: RouteRegistrar<'get'>;
/**
* 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
*/
post: RouteRegistrar;
post: RouteRegistrar<'post'>;
/**
* 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
*/
put: RouteRegistrar;
put: RouteRegistrar<'put'>;
/**
* Register a route handler for `PATCH` request.
* @param route {@link RouteConfig} - a route configuration.
* @param handler {@link RequestHandler} - a function to call to respond to an incoming request
*/
patch: RouteRegistrar<'patch'>;
/**
* 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
*/
delete: RouteRegistrar;
delete: RouteRegistrar<'delete'>;
/**
* Wrap a router handler to catch and converts legacy boom errors to proper custom errors.
@ -94,16 +113,19 @@ export interface IRouter {
) => RequestHandler<P, Q, B>;
/**
* Returns all routes registered with the this router.
* Returns all routes registered with this router.
* @returns List of registered routes.
* @internal
*/
getRoutes: () => RouterRoute[];
}
export type ContextEnhancer<P extends ObjectType, Q extends ObjectType, B extends ObjectType> = (
handler: RequestHandler<P, Q, B>
) => RequestHandlerEnhanced<P, Q, B>;
export type ContextEnhancer<
P extends ObjectType,
Q extends ObjectType,
B extends ObjectType,
Method extends RouteMethod
> = (handler: RequestHandler<P, Q, B, Method>) => RequestHandlerEnhanced<P, Q, B, Method>;
function getRouteFullPath(routerPath: string, routePath: string) {
// If router's path ends with slash and route's path starts with slash,
@ -121,8 +143,8 @@ function getRouteFullPath(routerPath: string, routePath: string) {
function routeSchemasFromRouteConfig<
P extends ObjectType,
Q extends ObjectType,
B extends ObjectType
>(route: RouteConfig<P, Q, B>, routeMethod: RouteMethod) {
B extends ObjectType | Type<Buffer> | Type<Stream>
>(route: RouteConfig<P, Q, B, typeof routeMethod>, routeMethod: RouteMethod) {
// The type doesn't allow `validate` to be undefined, but it can still
// happen when it's used from JavaScript.
if (route.validate === undefined) {
@ -144,6 +166,49 @@ function routeSchemasFromRouteConfig<
return route.validate ? route.validate : undefined;
}
/**
* Create a valid options object with "sensible" defaults + adding some validation to the options fields
*
* @param method HTTP verb for these options
* @param routeConfig The route config definition
*/
function validOptions(
method: RouteMethod,
routeConfig: RouteConfig<
ObjectType,
ObjectType,
ObjectType | Type<Buffer> | Type<Stream>,
typeof method
>
) {
const shouldNotHavePayload = ['head', 'get'].includes(method);
const { options = {}, validate } = routeConfig;
const shouldValidateBody = (validate && !!validate.body) || !!options.body;
const { output } = options.body || {};
if (typeof output === 'string' && !validBodyOutput.includes(output)) {
throw new Error(
`[options.body.output: '${output}'] in route ${method.toUpperCase()} ${
routeConfig.path
} is not valid. Only '${validBodyOutput.join("' or '")}' are valid.`
);
}
const body = shouldNotHavePayload
? undefined
: {
// If it's not a GET (requires payload) but no body validation is required (or no body options are specified),
// We assume the route does not care about the body => use the memory-cheapest approach (stream and no parsing)
output: !shouldValidateBody ? ('stream' as const) : undefined,
parse: !shouldValidateBody ? false : undefined,
// User's settings should overwrite any of the "desired" values
...options.body,
};
return { ...options, body };
}
/**
* @internal
*/
@ -153,21 +218,21 @@ export class Router implements IRouter {
public post: IRouter['post'];
public delete: IRouter['delete'];
public put: IRouter['put'];
public patch: IRouter['patch'];
constructor(
public readonly routerPath: string,
private readonly log: Logger,
private readonly enhanceWithContext: ContextEnhancer<any, any, any>
private readonly enhanceWithContext: ContextEnhancer<any, any, any, any>
) {
const buildMethod = (method: RouteMethod) => <
const buildMethod = <Method extends RouteMethod>(method: Method) => <
P extends ObjectType,
Q extends ObjectType,
B extends ObjectType
B extends ObjectType | Type<Buffer> | Type<Stream>
>(
route: RouteConfig<P, Q, B>,
handler: RequestHandler<P, Q, B>
route: RouteConfig<P, Q, B, Method>,
handler: RequestHandler<P, Q, B, Method>
) => {
const { path, options = {} } = route;
const routeSchemas = routeSchemasFromRouteConfig(route, method);
this.routes.push({
@ -179,8 +244,8 @@ export class Router implements IRouter {
handler: this.enhanceWithContext(handler),
}),
method,
path: getRouteFullPath(this.routerPath, path),
options,
path: getRouteFullPath(this.routerPath, route.path),
options: validOptions(method, route),
});
};
@ -188,6 +253,7 @@ export class Router implements IRouter {
this.post = buildMethod('post');
this.delete = buildMethod('delete');
this.put = buildMethod('put');
this.patch = buildMethod('patch');
}
public getRoutes() {
@ -200,7 +266,11 @@ export class Router implements IRouter {
return wrapErrors(handler);
}
private async handle<P extends ObjectType, Q extends ObjectType, B extends ObjectType>({
private async handle<
P extends ObjectType,
Q extends ObjectType,
B extends ObjectType | Type<Buffer> | Type<Stream>
>({
routeSchemas,
request,
responseToolkit,
@ -208,10 +278,10 @@ export class Router implements IRouter {
}: {
request: Request;
responseToolkit: ResponseToolkit;
handler: RequestHandlerEnhanced<P, Q, B>;
handler: RequestHandlerEnhanced<P, Q, B, typeof request.method>;
routeSchemas?: RouteSchemas<P, Q, B>;
}) {
let kibanaRequest: KibanaRequest<TypeOf<P>, TypeOf<Q>, TypeOf<B>>;
let kibanaRequest: KibanaRequest<TypeOf<P>, TypeOf<Q>, TypeOf<B>, typeof request.method>;
const hapiResponseAdapter = new HapiResponseAdapter(responseToolkit);
try {
kibanaRequest = KibanaRequest.from(request, routeSchemas);
@ -236,8 +306,9 @@ type WithoutHeadArgument<T> = T extends (first: any, ...rest: infer Params) => i
type RequestHandlerEnhanced<
P extends ObjectType,
Q extends ObjectType,
B extends ObjectType
> = WithoutHeadArgument<RequestHandler<P, Q, B>>;
B extends ObjectType | Type<Buffer> | Type<Stream>,
Method extends RouteMethod
> = WithoutHeadArgument<RequestHandler<P, Q, B, Method>>;
/**
* A function executed when route path matched requested resource path.
@ -272,8 +343,13 @@ type RequestHandlerEnhanced<
* ```
* @public
*/
export type RequestHandler<P extends ObjectType, Q extends ObjectType, B extends ObjectType> = (
export type RequestHandler<
P extends ObjectType,
Q extends ObjectType,
B extends ObjectType | Type<Buffer> | Type<Stream>,
Method extends RouteMethod = any
> = (
context: RequestHandlerContext,
request: KibanaRequest<TypeOf<P>, TypeOf<Q>, TypeOf<B>>,
request: KibanaRequest<TypeOf<P>, TypeOf<Q>, TypeOf<B>, Method>,
response: KibanaResponseFactory
) => IKibanaResponse<any> | Promise<IKibanaResponse<any>>;

View file

@ -93,6 +93,7 @@ export {
IsAuthenticated,
KibanaRequest,
KibanaRequestRoute,
KibanaRequestRouteOptions,
IKibanaResponse,
LifecycleResponseFactory,
KnownHeaders,
@ -112,9 +113,13 @@ export {
KibanaResponseFactory,
RouteConfig,
IRouter,
RouteRegistrar,
RouteMethod,
RouteConfigOptions,
RouteRegistrar,
RouteSchemas,
RouteConfigOptionsBody,
RouteContentType,
validBodyOutput,
SessionStorage,
SessionStorageCookieOptions,
SessionCookieValidationResult,

View file

@ -449,11 +449,11 @@ export interface AuthToolkit {
export class BasePath {
// @internal
constructor(serverBasePath?: string);
get: (request: KibanaRequest<unknown, unknown, unknown> | LegacyRequest) => string;
get: (request: KibanaRequest<unknown, unknown, unknown, any> | LegacyRequest) => string;
prepend: (path: string) => string;
remove: (path: string) => string;
readonly serverBasePath: string;
set: (request: KibanaRequest<unknown, unknown, unknown> | LegacyRequest, requestSpecificBasePath: string) => void;
set: (request: KibanaRequest<unknown, unknown, unknown, any> | LegacyRequest, requestSpecificBasePath: string) => void;
}
// Warning: (ae-forgotten-export) The symbol "BootstrapArgs" needs to be exported by the entry point index.d.ts
@ -714,15 +714,16 @@ export interface IndexSettingsDeprecationInfo {
// @public
export interface IRouter {
delete: RouteRegistrar;
get: RouteRegistrar;
delete: RouteRegistrar<'delete'>;
get: RouteRegistrar<'get'>;
// Warning: (ae-forgotten-export) The symbol "RouterRoute" needs to be exported by the entry point index.d.ts
//
// @internal
getRoutes: () => RouterRoute[];
handleLegacyErrors: <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(handler: RequestHandler<P, Q, B>) => RequestHandler<P, Q, B>;
post: RouteRegistrar;
put: RouteRegistrar;
patch: RouteRegistrar<'patch'>;
post: RouteRegistrar<'post'>;
put: RouteRegistrar<'put'>;
routerPath: string;
}
@ -746,37 +747,38 @@ export interface IUiSettingsClient {
}
// @public
export class KibanaRequest<Params = unknown, Query = unknown, Body = unknown> {
export class KibanaRequest<Params = unknown, Query = unknown, Body = unknown, Method extends RouteMethod = any> {
// @internal (undocumented)
protected readonly [requestSymbol]: Request;
constructor(request: Request, params: Params, query: Query, body: Body, withoutSecretHeaders: boolean);
// (undocumented)
readonly body: Body;
// Warning: (ae-forgotten-export) The symbol "RouteSchemas" needs to be exported by the entry point index.d.ts
//
// @internal
static from<P extends ObjectType, Q extends ObjectType, B extends ObjectType>(req: Request, routeSchemas?: RouteSchemas<P, Q, B>, withoutSecretHeaders?: boolean): KibanaRequest<P["type"], Q["type"], B["type"]>;
static from<P extends ObjectType, Q extends ObjectType, B extends ObjectType | Type<Buffer> | Type<Stream>>(req: Request, routeSchemas?: RouteSchemas<P, Q, B>, withoutSecretHeaders?: boolean): KibanaRequest<P["type"], Q["type"], B["type"], any>;
readonly headers: Headers;
// (undocumented)
readonly params: Params;
// (undocumented)
readonly query: Query;
readonly route: RecursiveReadonly<KibanaRequestRoute>;
readonly route: RecursiveReadonly<KibanaRequestRoute<Method>>;
// (undocumented)
readonly socket: IKibanaSocket;
readonly url: Url;
}
// @public
export interface KibanaRequestRoute {
export interface KibanaRequestRoute<Method extends RouteMethod> {
// (undocumented)
method: RouteMethod | 'patch' | 'options';
method: Method;
// (undocumented)
options: Required<RouteConfigOptions>;
options: KibanaRequestRouteOptions<Method>;
// (undocumented)
path: string;
}
// @public
export type KibanaRequestRouteOptions<Method extends RouteMethod> = Method extends 'get' | 'options' ? Required<Omit<RouteConfigOptions<Method>, 'body'>> : Required<RouteConfigOptions<Method>>;
// @public
export type KibanaResponseFactory = typeof kibanaResponseFactory;
@ -1043,7 +1045,7 @@ export type RedirectResponseOptions = HttpResponseOptions & {
};
// @public
export type RequestHandler<P extends ObjectType, Q extends ObjectType, B extends ObjectType> = (context: RequestHandlerContext, request: KibanaRequest<TypeOf<P>, TypeOf<Q>, TypeOf<B>>, response: KibanaResponseFactory) => IKibanaResponse<any> | Promise<IKibanaResponse<any>>;
export type RequestHandler<P extends ObjectType, Q extends ObjectType, B extends ObjectType | Type<Buffer> | Type<Stream>, Method extends RouteMethod = any> = (context: RequestHandlerContext, request: KibanaRequest<TypeOf<P>, TypeOf<Q>, TypeOf<B>, Method>, response: KibanaResponseFactory) => IKibanaResponse<any> | Promise<IKibanaResponse<any>>;
// @public
export interface RequestHandlerContext {
@ -1085,23 +1087,45 @@ export type ResponseHeaders = {
};
// @public
export interface RouteConfig<P extends ObjectType, Q extends ObjectType, B extends ObjectType> {
options?: RouteConfigOptions;
export interface RouteConfig<P extends ObjectType, Q extends ObjectType, B extends ObjectType | Type<Buffer> | Type<Stream>, Method extends RouteMethod> {
options?: RouteConfigOptions<Method>;
path: string;
validate: RouteSchemas<P, Q, B> | false;
}
// @public
export interface RouteConfigOptions {
export interface RouteConfigOptions<Method extends RouteMethod> {
authRequired?: boolean;
body?: Method extends 'get' | 'options' ? undefined : RouteConfigOptionsBody;
tags?: readonly string[];
}
// @public
export type RouteMethod = 'get' | 'post' | 'put' | 'delete';
export interface RouteConfigOptionsBody {
accepts?: RouteContentType | RouteContentType[] | string | string[];
maxBytes?: number;
output?: typeof validBodyOutput[number];
parse?: boolean | 'gunzip';
}
// @public
export type RouteRegistrar = <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>) => void;
export type RouteContentType = 'application/json' | 'application/*+json' | 'application/octet-stream' | 'application/x-www-form-urlencoded' | 'multipart/form-data' | 'text/*';
// @public
export type RouteMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options';
// @public
export type RouteRegistrar<Method extends RouteMethod> = <P extends ObjectType, Q extends ObjectType, B extends ObjectType | Type<Buffer> | Type<Stream>>(route: RouteConfig<P, Q, B, Method>, handler: RequestHandler<P, Q, B, Method>) => void;
// @public
export interface RouteSchemas<P extends ObjectType, Q extends ObjectType, B extends ObjectType | Type<Buffer> | Type<Stream>> {
// (undocumented)
body?: B;
// (undocumented)
params?: P;
// (undocumented)
query?: Q;
}
// @public (undocumented)
export interface SavedObject<T extends SavedObjectAttributes = any> {
@ -1633,6 +1657,9 @@ export interface UserProvidedValues<T extends SavedObjectAttribute = any> {
userValue?: T;
}
// @public
export const validBodyOutput: readonly ["data", "stream"];
// Warnings were encountered during analysis:
//

View file

@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema';
import * as t from 'io-ts';
import { PathReporter } from 'io-ts/lib/PathReporter';
import { isLeft } from 'fp-ts/lib/Either';
import { KibanaResponseFactory } from 'src/core/server';
import { KibanaResponseFactory, RouteRegistrar } from 'src/core/server';
import { APMConfig } from '../../../../../../plugins/apm/server';
import {
ServerAPI,
@ -65,7 +65,7 @@ export function createApi() {
body: bodyRt && 'props' in bodyRt ? t.exact(bodyRt) : fallbackBodyRt
};
router[routerMethod](
(router[routerMethod] as RouteRegistrar<typeof routerMethod>)(
{
path,
options,

View file

@ -41,8 +41,8 @@ describe('SAML authentication routes', () => {
});
describe('Assertion consumer service endpoint', () => {
let routeHandler: RequestHandler<any, any, any>;
let routeConfig: RouteConfig<any, any, any>;
let routeHandler: RequestHandler<any, any, any, any>;
let routeConfig: RouteConfig<any, any, any, any>;
beforeEach(() => {
const [acsRouteConfig, acsRouteHandler] = router.post.mock.calls.find(
([{ path }]) => path === '/api/security/saml/callback'