diff --git a/src/core/packages/http/router-server-internal/index.ts b/src/core/packages/http/router-server-internal/index.ts
index 0e048c59144df..f0cd26b1ba90b 100644
--- a/src/core/packages/http/router-server-internal/index.ts
+++ b/src/core/packages/http/router-server-internal/index.ts
@@ -16,7 +16,7 @@ export {
type HandlerResolutionStrategy,
} from './src/versioned_router';
export { Router } from './src/router';
-export type { RouterOptions, InternalRegistrar, InternalRegistrarOptions } from './src/router';
+export type { RouterOptions } from './src/router';
export { isKibanaRequest, isRealRequest, ensureRawRequest, CoreKibanaRequest } from './src/request';
export { isSafeMethod } from './src/route';
export { HapiResponseAdapter } from './src/response_adapter';
diff --git a/src/core/packages/http/router-server-internal/src/request.ts b/src/core/packages/http/router-server-internal/src/request.ts
index 24c3aa7c61d3f..1eaae21c13259 100644
--- a/src/core/packages/http/router-server-internal/src/request.ts
+++ b/src/core/packages/http/router-server-internal/src/request.ts
@@ -191,7 +191,7 @@ export class CoreKibanaRequest<
enumerable: false,
});
- this.httpVersion = isRealReq ? request.raw.req.httpVersion : '1.0';
+ this.httpVersion = isRealReq ? getHttpVersionFromRequest(request) : '1.0';
this.apiVersion = undefined;
this.protocol = getProtocolFromHttpVersion(this.httpVersion);
@@ -418,3 +418,11 @@ function sanitizeRequest(req: Request): { query: unknown; params: unknown; body:
function getProtocolFromHttpVersion(httpVersion: string): HttpProtocol {
return httpVersion.split('.')[0] === '2' ? 'http2' : 'http1';
}
+
+function getHttpVersionFromRequest(request: Request) {
+ return request.raw.req.httpVersion;
+}
+
+export function getProtocolFromRequest(request: Request) {
+ return getProtocolFromHttpVersion(getHttpVersionFromRequest(request));
+}
diff --git a/src/core/packages/http/router-server-internal/src/route.test.ts b/src/core/packages/http/router-server-internal/src/route.test.ts
new file mode 100644
index 0000000000000..3e61235347ab4
--- /dev/null
+++ b/src/core/packages/http/router-server-internal/src/route.test.ts
@@ -0,0 +1,188 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import { hapiMocks } from '@kbn/hapi-mocks';
+import { validateHapiRequest, handle } from './route';
+import { createRouter } from './versioned_router/mocks';
+import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
+import { Logger } from '@kbn/logging';
+import { RouteValidator } from './validator';
+import { schema } from '@kbn/config-schema';
+import { Router } from './router';
+import { RouteAccess } from '@kbn/core-http-server';
+import { createRequest } from './versioned_router/core_versioned_route.test.util';
+import { kibanaResponseFactory } from './response';
+
+describe('handle', () => {
+ let handler: jest.Func;
+ let log: Logger;
+ let router: Router;
+ beforeEach(() => {
+ router = createRouter();
+ handler = jest.fn(async () => kibanaResponseFactory.ok());
+ log = loggingSystemMock.createLogger();
+ });
+ describe('post validation events', () => {
+ it('emits with validation schemas provided', async () => {
+ const validate = { body: schema.object({ foo: schema.number() }) };
+ await handle(createRequest({ body: { foo: 1 } }), {
+ router,
+ handler,
+ log,
+ method: 'get',
+ route: { path: '/test', validate },
+ routeSchemas: RouteValidator.from(validate),
+ });
+ // Failure
+ await handle(createRequest({ body: { foo: 'bar' } }), {
+ router,
+ handler,
+ log,
+ method: 'get',
+ route: {
+ path: '/test',
+ validate,
+ options: {
+ deprecated: {
+ severity: 'warning',
+ reason: { type: 'bump', newApiVersion: '123' },
+ documentationUrl: 'http://test.foo',
+ },
+ },
+ },
+ routeSchemas: RouteValidator.from(validate),
+ });
+
+ expect(handler).toHaveBeenCalledTimes(1);
+ expect(router.emitPostValidate).toHaveBeenCalledTimes(2);
+
+ expect(router.emitPostValidate).toHaveBeenNthCalledWith(1, expect.any(Object), {
+ deprecated: undefined,
+ isInternalApiRequest: false,
+ isPublicAccess: false,
+ });
+ expect(router.emitPostValidate).toHaveBeenNthCalledWith(2, expect.any(Object), {
+ deprecated: {
+ severity: 'warning',
+ reason: { type: 'bump', newApiVersion: '123' },
+ documentationUrl: 'http://test.foo',
+ },
+ isInternalApiRequest: false,
+ isPublicAccess: false,
+ });
+ });
+
+ it('emits with no validation schemas provided', async () => {
+ await handle(createRequest({ body: { foo: 1 } }), {
+ router,
+ handler,
+ log,
+ method: 'get',
+ route: {
+ path: '/test',
+ validate: false,
+ options: {
+ deprecated: {
+ severity: 'warning',
+ reason: { type: 'bump', newApiVersion: '123' },
+ documentationUrl: 'http://test.foo',
+ },
+ },
+ },
+ routeSchemas: undefined,
+ });
+
+ expect(handler).toHaveBeenCalledTimes(1);
+ expect(router.emitPostValidate).toHaveBeenCalledTimes(1);
+
+ expect(router.emitPostValidate).toHaveBeenCalledWith(expect.any(Object), {
+ deprecated: {
+ severity: 'warning',
+ reason: { type: 'bump', newApiVersion: '123' },
+ documentationUrl: 'http://test.foo',
+ },
+ isInternalApiRequest: false,
+ isPublicAccess: false,
+ });
+ });
+ });
+});
+
+describe('validateHapiRequest', () => {
+ let router: Router;
+ let log: Logger;
+ beforeEach(() => {
+ router = createRouter();
+ log = loggingSystemMock.createLogger();
+ });
+ it('validates hapi requests and returns kibana requests: ok case', () => {
+ const { ok, error } = validateHapiRequest(hapiMocks.createRequest({ payload: { ok: true } }), {
+ log,
+ routeInfo: { access: 'public', httpResource: false },
+ router,
+ routeSchemas: RouteValidator.from({ body: schema.object({ ok: schema.literal(true) }) }),
+ });
+ expect(ok?.body).toEqual({ ok: true });
+ expect(error).toBeUndefined();
+ expect(log.error).not.toHaveBeenCalled();
+ });
+ it('validates hapi requests and returns kibana requests: error case', () => {
+ const { ok, error } = validateHapiRequest(hapiMocks.createRequest({ payload: { ok: false } }), {
+ log,
+ routeInfo: { access: 'public', httpResource: false },
+ router,
+ routeSchemas: RouteValidator.from({ body: schema.object({ ok: schema.literal(true) }) }),
+ });
+ expect(ok).toBeUndefined();
+ expect(error?.status).toEqual(400);
+ expect(error?.payload).toMatch(/expected value to equal/);
+ expect(log.error).toHaveBeenCalledTimes(1);
+ expect(log.error).toHaveBeenCalledWith('400 Bad Request', {
+ error: { message: '[request body.ok]: expected value to equal [true]' },
+ http: { request: { method: undefined, path: undefined }, response: { status_code: 400 } },
+ });
+ });
+
+ it('emits post validation events on the router', () => {
+ const deps = {
+ log,
+ routeInfo: { access: 'public' as RouteAccess, httpResource: false },
+ router,
+ routeSchemas: RouteValidator.from({ body: schema.object({ ok: schema.literal(true) }) }),
+ };
+ {
+ const { ok, error } = validateHapiRequest(
+ hapiMocks.createRequest({ payload: { ok: false } }),
+ deps
+ );
+ expect(ok).toBeUndefined();
+ expect(error).toBeDefined();
+ expect(router.emitPostValidate).toHaveBeenCalledTimes(1);
+ expect(router.emitPostValidate).toHaveBeenCalledWith(expect.any(Object), {
+ deprecated: undefined,
+ isInternalApiRequest: false,
+ isPublicAccess: true,
+ });
+ }
+ {
+ const { ok, error } = validateHapiRequest(
+ hapiMocks.createRequest({ payload: { ok: true } }),
+ deps
+ );
+ expect(ok).toBeDefined();
+ expect(error).toBeUndefined();
+ expect(router.emitPostValidate).toHaveBeenCalledTimes(2);
+ expect(router.emitPostValidate).toHaveBeenNthCalledWith(2, expect.any(Object), {
+ deprecated: undefined,
+ isInternalApiRequest: false,
+ isPublicAccess: true,
+ });
+ }
+ });
+});
diff --git a/src/core/packages/http/router-server-internal/src/route.ts b/src/core/packages/http/router-server-internal/src/route.ts
index 6faae2c1816b9..9afc11435eea7 100644
--- a/src/core/packages/http/router-server-internal/src/route.ts
+++ b/src/core/packages/http/router-server-internal/src/route.ts
@@ -7,8 +7,40 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-import type { RouteMethod, SafeRouteMethod, RouteConfig } from '@kbn/core-http-server';
-import type { RouteSecurityGetter, RouteSecurity } from '@kbn/core-http-server';
+import {
+ type RouteMethod,
+ type SafeRouteMethod,
+ type RouteConfig,
+ getRequestValidation,
+} from '@kbn/core-http-server';
+import type {
+ RouteSecurityGetter,
+ RouteSecurity,
+ AnyKibanaRequest,
+ IKibanaResponse,
+ RouteAccess,
+ RouteConfigOptions,
+} from '@kbn/core-http-server';
+import { isConfigSchema } from '@kbn/config-schema';
+import { isZod } from '@kbn/zod';
+import type { Logger } from '@kbn/logging';
+import type { DeepPartial } from '@kbn/utility-types';
+import { Request } from '@hapi/hapi';
+import { Mutable } from 'utility-types';
+import type { InternalRouterRoute, RequestHandlerEnhanced, Router } from './router';
+import { CoreKibanaRequest } from './request';
+import { RouteValidator } from './validator';
+import { BASE_PUBLIC_VERSION } from './versioned_router';
+import { kibanaResponseFactory } from './response';
+import {
+ getVersionHeader,
+ injectVersionHeader,
+ formatErrorMeta,
+ getRouteFullPath,
+ validOptions,
+ prepareRouteConfigValidation,
+} from './util';
+import { validRouteSecurity } from './security_route_config_validator';
export function isSafeMethod(method: RouteMethod): method is SafeRouteMethod {
return method === 'get' || method === 'options';
@@ -21,3 +53,162 @@ export type InternalRouteConfig
= Omit<
> & {
security?: RouteSecurityGetter | RouteSecurity;
};
+
+/** @internal */
+interface Dependencies {
+ router: Router;
+ route: InternalRouteConfig;
+ handler: RequestHandlerEnhanced;
+ log: Logger;
+ method: RouteMethod;
+}
+
+export function buildRoute({
+ handler,
+ log,
+ route,
+ router,
+ method,
+}: Dependencies): InternalRouterRoute {
+ route = prepareRouteConfigValidation(route);
+ const routeSchemas = routeSchemasFromRouteConfig(route, method);
+ return {
+ handler: async (req) => {
+ return await handle(req, {
+ handler,
+ log,
+ method,
+ route,
+ router,
+ routeSchemas,
+ });
+ },
+ method,
+ path: getRouteFullPath(router.routerPath, route.path),
+ options: validOptions(method, route),
+ security: validRouteSecurity(route.security as DeepPartial, route.options),
+ validationSchemas: route.validate,
+ isVersioned: false,
+ };
+}
+
+/** @internal */
+interface HandlerDependencies extends Dependencies {
+ routeSchemas?: RouteValidator;
+}
+
+type RouteInfo = Pick, 'access' | 'httpResource' | 'deprecated'>;
+
+interface ValidationContext {
+ routeInfo: RouteInfo;
+ router: Router;
+ log: Logger;
+ routeSchemas?: RouteValidator;
+ version?: string;
+}
+
+/** @internal */
+export function validateHapiRequest(
+ request: Request,
+ { routeInfo, router, log, routeSchemas, version }: ValidationContext
+): { ok: AnyKibanaRequest; error?: never } | { ok?: never; error: IKibanaResponse } {
+ let kibanaRequest: Mutable;
+ try {
+ kibanaRequest = CoreKibanaRequest.from(request, routeSchemas);
+ kibanaRequest.apiVersion = version;
+ } catch (error) {
+ kibanaRequest = CoreKibanaRequest.from(request);
+ kibanaRequest.apiVersion = version;
+
+ log.error('400 Bad Request', formatErrorMeta(400, { request, error }));
+
+ const response = kibanaResponseFactory.badRequest({
+ body: error.message,
+ headers: isPublicAccessApiRoute(routeInfo)
+ ? getVersionHeader(BASE_PUBLIC_VERSION)
+ : undefined,
+ });
+ return { error: response };
+ } finally {
+ router.emitPostValidate(
+ kibanaRequest!,
+ getPostValidateEventMetadata(kibanaRequest!, routeInfo)
+ );
+ }
+
+ return { ok: kibanaRequest };
+}
+
+/** @internal */
+export const handle = async (
+ request: Request,
+ { router, route, handler, routeSchemas, log }: HandlerDependencies
+) => {
+ const { error, ok: kibanaRequest } = validateHapiRequest(request, {
+ routeInfo: {
+ access: route.options?.access,
+ httpResource: route.options?.httpResource,
+ deprecated: route.options?.deprecated,
+ },
+ router,
+ log,
+ routeSchemas,
+ });
+ if (error) {
+ return error;
+ }
+ const kibanaResponse = await handler(kibanaRequest, kibanaResponseFactory);
+ if (isPublicAccessApiRoute(route.options)) {
+ injectVersionHeader(BASE_PUBLIC_VERSION, kibanaResponse);
+ }
+ return kibanaResponse;
+};
+
+function isPublicAccessApiRoute({
+ access,
+ httpResource,
+}: {
+ access?: RouteAccess;
+ httpResource?: boolean;
+} = {}): boolean {
+ return !httpResource && access === 'public';
+}
+
+/**
+ * Create the validation schemas for a route
+ *
+ * @returns Route schemas if `validate` is specified on the route, otherwise
+ * undefined.
+ */
+function routeSchemasFromRouteConfig(
+ route: InternalRouteConfig
,
+ 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) {
+ throw new Error(
+ `The [${routeMethod}] at [${route.path}] does not have a 'validate' specified. Use 'false' as the value if you want to bypass validation.`
+ );
+ }
+
+ if (route.validate !== false) {
+ const validation = getRequestValidation(route.validate);
+ Object.entries(validation).forEach(([key, schema]) => {
+ if (!(isConfigSchema(schema) || isZod(schema) || typeof schema === 'function')) {
+ throw new Error(
+ `Expected a valid validation logic declared with '@kbn/config-schema' package, '@kbn/zod' package or a RouteValidationFunction at key: [${key}].`
+ );
+ }
+ });
+ return RouteValidator.from(validation);
+ }
+}
+
+function getPostValidateEventMetadata(request: AnyKibanaRequest, routeInfo: RouteInfo) {
+ return {
+ deprecated: routeInfo.deprecated,
+ isInternalApiRequest: request.isInternalApiRequest,
+ isPublicAccess: routeInfo.access === 'public',
+ };
+}
diff --git a/src/core/packages/http/router-server-internal/src/router.test.ts b/src/core/packages/http/router-server-internal/src/router.test.ts
index f0eaa96879d42..ee1b3d234b71f 100644
--- a/src/core/packages/http/router-server-internal/src/router.test.ts
+++ b/src/core/packages/http/router-server-internal/src/router.test.ts
@@ -95,14 +95,12 @@ describe('Router', () => {
it('can exclude versioned routes', () => {
const router = new Router('', logger, enhanceWithContext, routerOptions);
const validation = schema.object({ foo: schema.string() });
- router.post(
- {
+ router.versioned
+ .post({
path: '/versioned',
- validate: { body: validation, query: validation, params: validation },
- },
- (context, req, res) => res.ok(),
- { isVersioned: true, events: false }
- );
+ access: 'internal',
+ })
+ .addVersion({ version: '999', validate: false }, async (ctx, req, res) => res.ok());
router.get(
{
path: '/unversioned',
diff --git a/src/core/packages/http/router-server-internal/src/router.ts b/src/core/packages/http/router-server-internal/src/router.ts
index 7c7a67facaddc..fcf70341b0c41 100644
--- a/src/core/packages/http/router-server-internal/src/router.ts
+++ b/src/core/packages/http/router-server-internal/src/router.ts
@@ -10,7 +10,6 @@
import { EventEmitter } from 'node:events';
import type { Request, ResponseToolkit } from '@hapi/hapi';
import apm from 'elastic-apm-node';
-import { isConfigSchema } from '@kbn/config-schema';
import type { Logger } from '@kbn/logging';
import {
isUnauthorizedError as isElasticsearchUnauthorizedError,
@@ -27,24 +26,18 @@ import type {
RequestHandler,
VersionedRouter,
RouteRegistrar,
- RouteSecurity,
PostValidationMetadata,
+ IKibanaResponse,
} from '@kbn/core-http-server';
-import { isZod } from '@kbn/zod';
-import { validBodyOutput, getRequestValidation } from '@kbn/core-http-server';
import type { RouteSecurityGetter } from '@kbn/core-http-server';
-import type { DeepPartial } from '@kbn/utility-types';
-import { RouteValidator } from './validator';
-import { BASE_PUBLIC_VERSION, CoreVersionedRouter } from './versioned_router';
-import { CoreKibanaRequest } from './request';
+import { CoreVersionedRouter } from './versioned_router';
+import { CoreKibanaRequest, getProtocolFromRequest } from './request';
import { kibanaResponseFactory } from './response';
import { HapiResponseAdapter } from './response_adapter';
import { wrapErrors } from './error_wrapper';
-import { Method } from './versioned_router/types';
-import { getVersionHeader, injectVersionHeader, prepareRouteConfigValidation } from './util';
+import { formatErrorMeta } from './util';
import { stripIllegalHttp2Headers } from './strip_illegal_http2_headers';
-import { validRouteSecurity } from './security_route_config_validator';
-import { InternalRouteConfig } from './route';
+import { InternalRouteConfig, buildRoute } from './route';
export type ContextEnhancer<
P,
@@ -54,86 +47,28 @@ export type ContextEnhancer<
Context extends RequestHandlerContextBase
> = (handler: RequestHandler
) => RequestHandlerEnhanced
;
-export function getRouteFullPath(routerPath: string, routePath: string) {
- // If router's path ends with slash and route's path starts with slash,
- // we should omit one of them to have a valid concatenated path.
- const routePathStartIndex = routerPath.endsWith('/') && routePath.startsWith('/') ? 1 : 0;
- return `${routerPath}${routePath.slice(routePathStartIndex)}`;
-}
+/** @internal */
+export type InternalRouteHandler = (request: Request) => Promise;
/**
- * Create the validation schemas for a route
+ * We have at least two implementations of InternalRouterRoutes:
+ * (1) Router route
+ * (2) Versioned router route {@link CoreVersionedRoute}
*
- * @returns Route schemas if `validate` is specified on the route, otherwise
- * undefined.
- */
-function routeSchemasFromRouteConfig(
- route: InternalRouteConfig
,
- 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) {
- throw new Error(
- `The [${routeMethod}] at [${route.path}] does not have a 'validate' specified. Use 'false' as the value if you want to bypass validation.`
- );
- }
-
- if (route.validate !== false) {
- const validation = getRequestValidation(route.validate);
- Object.entries(validation).forEach(([key, schema]) => {
- if (!(isConfigSchema(schema) || isZod(schema) || typeof schema === 'function')) {
- throw new Error(
- `Expected a valid validation logic declared with '@kbn/config-schema' package, '@kbn/zod' package or a RouteValidationFunction at key: [${key}].`
- );
- }
- });
- return RouteValidator.from(validation);
- }
-}
-
-/**
- * Create a valid options object with "sensible" defaults + adding some validation to the options fields
+ * The former registers internal handlers when users call `route.put(...)` while
+ * the latter registers an internal handler for `router.versioned.put(...)`.
+ *
+ * This enables us to expose internal details to each of these types routes so
+ * that implementation has freedom to change what it needs to in each case, like:
*
- * @param method HTTP verb for these options
- * @param routeConfig The route config definition
+ * validation: versioned routes only know what validation to run after inspecting
+ * special version values, whereas "regular" routes only ever have one validation
+ * that is predetermined to always run.
+ * @internal
*/
-function validOptions(
- method: RouteMethod,
- routeConfig: InternalRouteConfig
-) {
- const shouldNotHavePayload = ['head', 'get'].includes(method);
- const { options = {}, validate } = routeConfig;
- const shouldValidateBody = (validate && !!getRequestValidation(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.`
- );
- }
-
- // @ts-expect-error to eliminate problems with `security` in the options for route factories abstractions
- if (options.security) {
- throw new Error('`options.security` is not allowed in route config. Use `security` instead.');
- }
-
- 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 };
-}
+export type InternalRouterRoute = Omit & {
+ handler: InternalRouteHandler;
+};
/** @internal */
export interface RouterOptions {
@@ -152,17 +87,6 @@ export interface RouterOptions {
};
}
-/** @internal */
-export interface InternalRegistrarOptions {
- /** @default false */
- isVersioned: boolean;
- /**
- * Whether this route should emit "route events" like postValidate
- * @default true
- */
- events: boolean;
-}
-
/** @internal */
export type VersionedRouteConfig = Omit<
RouteConfig
,
@@ -171,13 +95,6 @@ export type VersionedRouteConfig
= Omit<
security?: RouteSecurityGetter;
};
-/** @internal */
-export type InternalRegistrar = (
- route: InternalRouteConfig
,
- handler: RequestHandler
,
- internalOpts?: InternalRegistrarOptions
-) => ReturnType>;
-
/** @internal */
type RouterEvents =
/** Called after route validation, regardless of success or failure */
@@ -189,19 +106,24 @@ type RouterEvents =
export class Router
implements IRouter
{
- private static ee = new EventEmitter();
+ /**
+ * Used for global request events at the router level, similar to what we get from Hapi's request lifecycle events.
+ *
+ * See {@link RouterEvents}.
+ */
+ private static events = new EventEmitter();
public routes: Array> = [];
public pluginId?: symbol;
- public get: InternalRegistrar<'get', Context>;
- public post: InternalRegistrar<'post', Context>;
- public delete: InternalRegistrar<'delete', Context>;
- public put: InternalRegistrar<'put', Context>;
- public patch: InternalRegistrar<'patch', Context>;
+ public get: RouteRegistrar<'get', Context>;
+ public post: RouteRegistrar<'post', Context>;
+ public delete: RouteRegistrar<'delete', Context>;
+ public put: RouteRegistrar<'put', Context>;
+ public patch: RouteRegistrar<'patch', Context>;
constructor(
public readonly routerPath: string,
private readonly log: Logger,
- private readonly enhanceWithContext: ContextEnhancer,
+ public readonly enhanceWithContext: ContextEnhancer,
private readonly options: RouterOptions
) {
this.pluginId = options.pluginId;
@@ -209,40 +131,17 @@ export class Router(method: Method) =>
(
route: InternalRouteConfig
,
- handler: RequestHandler
,
- { isVersioned, events }: InternalRegistrarOptions = { isVersioned: false, events: true }
+ handler: RequestHandler
) => {
- route = prepareRouteConfigValidation(route);
- const routeSchemas = routeSchemasFromRouteConfig(route, method);
- const isPublicUnversionedRoute =
- !isVersioned &&
- route.options?.access === 'public' &&
- // We do not consider HTTP resource routes as APIs
- route.options?.httpResource !== true;
-
- this.routes.push({
- handler: async (req, responseToolkit) => {
- return await this.handle({
- routeSchemas,
- request: req,
- responseToolkit,
- isPublicUnversionedRoute,
- handler: this.enhanceWithContext(handler),
- emit: events ? { onPostValidation: this.emitPostValidate } : undefined,
- });
- },
- method,
- path: getRouteFullPath(this.routerPath, route.path),
- options: validOptions(method, route),
- // For the versioned route security is validated in the versioned router
- security: isVersioned
- ? route.security
- : validRouteSecurity(route.security as DeepPartial, route.options),
- validationSchemas: route.validate,
- // @ts-expect-error using isVersioned: false in the type instead of boolean
- // for typeguarding between versioned and unversioned RouterRoute types
- isVersioned,
- });
+ this.registerRoute(
+ buildRoute({
+ handler: this.enhanceWithContext(handler),
+ log: this.log,
+ method,
+ route,
+ router: this,
+ })
+ );
};
this.get = buildMethod('get');
@@ -253,11 +152,11 @@ export class Router void) {
- Router.ee.on(event, cb);
+ Router.events.on(event, cb);
}
public static off(event: RouterEvents, cb: (req: CoreKibanaRequest, ...args: any[]) => void) {
- Router.ee.off(event, cb);
+ Router.events.off(event, cb);
}
public getRoutes({ excludeVersionedRoutes }: { excludeVersionedRoutes?: boolean } = {}) {
@@ -269,27 +168,6 @@ export class Router {
const postValidate: RouterEvents = 'onPostValidate';
- Router.ee.emit(postValidate, request, postValidateConext);
+ Router.events.emit(postValidate, request, postValidateConext);
};
- private async handle({
- routeSchemas,
+ /** @internal */
+ public registerRoute(route: InternalRouterRoute) {
+ this.routes.push({
+ ...route,
+ handler: async (request, responseToolkit) =>
+ await this.handle({ request, responseToolkit, handler: route.handler }),
+ });
+ }
+
+ private async handle({
request,
responseToolkit,
- emit,
- isPublicUnversionedRoute,
handler,
}: {
request: Request;
responseToolkit: ResponseToolkit;
- emit?: {
- onPostValidation: (req: KibanaRequest, metadata: PostValidationMetadata) => void;
- };
- isPublicUnversionedRoute: boolean;
- handler: RequestHandlerEnhanced<
- P,
- Q,
- B,
- // request.method's type contains way more verbs than we currently support
- typeof request.method extends RouteMethod ? typeof request.method : any
- >;
- routeSchemas?: RouteValidator
;
+ handler: InternalRouteHandler;
}) {
- let kibanaRequest: KibanaRequest<
- P,
- Q,
- B,
- typeof request.method extends RouteMethod ? typeof request.method : any
- >;
const hapiResponseAdapter = new HapiResponseAdapter(responseToolkit);
try {
- kibanaRequest = CoreKibanaRequest.from(request, routeSchemas) as KibanaRequest<
- P,
- Q,
- B,
- typeof request.method extends RouteMethod ? typeof request.method : any
- >;
- } catch (error) {
- this.logError('400 Bad Request', 400, { request, error });
- const response = hapiResponseAdapter.toBadRequest(error.message);
- if (isPublicUnversionedRoute) {
- response.output.headers = {
- ...response.output.headers,
- ...getVersionHeader(BASE_PUBLIC_VERSION),
- };
- }
-
- // Emit onPostValidation even if validation fails.
- const req = CoreKibanaRequest.from(request);
- emit?.onPostValidation(req, {
- deprecated: req.route.options.deprecated,
- isInternalApiRequest: req.isInternalApiRequest,
- isPublicAccess: req.route.options.access === 'public',
- });
- return response;
- }
-
- emit?.onPostValidation(kibanaRequest, {
- deprecated: kibanaRequest.route.options.deprecated,
- isInternalApiRequest: kibanaRequest.isInternalApiRequest,
- isPublicAccess: kibanaRequest.route.options.access === 'public',
- });
-
- try {
- const kibanaResponse = await handler(kibanaRequest, kibanaResponseFactory);
- if (isPublicUnversionedRoute) {
- injectVersionHeader(BASE_PUBLIC_VERSION, kibanaResponse);
- }
- if (kibanaRequest.protocol === 'http2' && kibanaResponse.options.headers) {
+ const kibanaResponse = await handler(request);
+ if (getProtocolFromRequest(request) === 'http2' && kibanaResponse.options.headers) {
kibanaResponse.options.headers = stripIllegalHttp2Headers({
headers: kibanaResponse.options.headers,
isDev: this.options.isDev ?? false,
@@ -384,14 +215,14 @@ export class Router = T extends (first: any, ...rest: infer Params) => i
? (...rest: Params) => Return
: never;
-type RequestHandlerEnhanced = WithoutHeadArgument<
+export type RequestHandlerEnhanced
= WithoutHeadArgument<
RequestHandler
>;
diff --git a/src/core/packages/http/router-server-internal/src/util.ts b/src/core/packages/http/router-server-internal/src/util.ts
index 176d33b589880..b4027d9211890 100644
--- a/src/core/packages/http/router-server-internal/src/util.ts
+++ b/src/core/packages/http/router-server-internal/src/util.ts
@@ -13,10 +13,13 @@ import {
type RouteValidatorFullConfigResponse,
type RouteMethod,
type RouteValidator,
+ getRequestValidation,
+ validBodyOutput,
} from '@kbn/core-http-server';
import type { Mutable } from 'utility-types';
-import type { IKibanaResponse, ResponseHeaders } from '@kbn/core-http-server';
+import type { IKibanaResponse, ResponseHeaders, SafeRouteMethod } from '@kbn/core-http-server';
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
+import { Request } from '@hapi/hapi';
import type { InternalRouteConfig } from './route';
function isStatusCode(key: string) {
@@ -92,3 +95,76 @@ export function getVersionHeader(version: string): ResponseHeaders {
export function injectVersionHeader(version: string, response: IKibanaResponse): IKibanaResponse {
return injectResponseHeaders(getVersionHeader(version), response);
}
+
+export function formatErrorMeta(
+ statusCode: number,
+ {
+ error,
+ request,
+ }: {
+ error: Error;
+ request: Request;
+ }
+) {
+ return {
+ http: {
+ response: { status_code: statusCode },
+ request: { method: request.route?.method, path: request.route?.path },
+ },
+ error: { message: error.message },
+ };
+}
+
+export function getRouteFullPath(routerPath: string, routePath: string) {
+ // If router's path ends with slash and route's path starts with slash,
+ // we should omit one of them to have a valid concatenated path.
+ const routePathStartIndex = routerPath.endsWith('/') && routePath.startsWith('/') ? 1 : 0;
+ return `${routerPath}${routePath.slice(routePathStartIndex)}`;
+}
+
+export function isSafeMethod(method: RouteMethod): method is SafeRouteMethod {
+ return method === 'get' || method === 'options';
+}
+
+/**
+ * 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
+ */
+export function validOptions(
+ method: RouteMethod,
+ routeConfig: InternalRouteConfig
+) {
+ const shouldNotHavePayload = ['head', 'get'].includes(method);
+ const { options = {}, validate } = routeConfig;
+ const shouldValidateBody = (validate && !!getRequestValidation(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.`
+ );
+ }
+
+ // @ts-expect-error to eliminate problems with `security` in the options for route factories abstractions
+ if (options.security) {
+ throw new Error('`options.security` is not allowed in route config. Use `security` instead.');
+ }
+
+ 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 };
+}
diff --git a/src/core/packages/http/router-server-internal/src/versioned_router/core_versioned_route.test.ts b/src/core/packages/http/router-server-internal/src/versioned_router/core_versioned_route.test.ts
index 9c09de9e45895..a9f90fdc0e0b2 100644
--- a/src/core/packages/http/router-server-internal/src/versioned_router/core_versioned_route.test.ts
+++ b/src/core/packages/http/router-server-internal/src/versioned_router/core_versioned_route.test.ts
@@ -9,43 +9,31 @@
import type { ApiVersion } from '@kbn/core-http-common';
import type {
- KibanaResponseFactory,
RequestHandler,
- RouteConfig,
VersionedRouteValidation,
RouteSecurity,
} from '@kbn/core-http-server';
-import { Router } from '../router';
+import { InternalRouteHandler, Router } from '../router';
import { createFooValidation } from '../router.test.util';
import { createRouter } from './mocks';
import { CoreVersionedRouter, unwrapVersionedResponseBodyValidation } from '.';
-import { passThroughValidation } from './core_versioned_route';
-import { Method } from './types';
import { createRequest } from './core_versioned_route.test.util';
import { isConfigSchema } from '@kbn/config-schema';
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
+import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
describe('Versioned route', () => {
let router: Router;
- let responseFactory: jest.Mocked;
+ let versionedRouter: CoreVersionedRouter;
let testValidation: ReturnType;
const handlerFn: RequestHandler = async (ctx, req, res) => res.ok({ body: { foo: 1 } });
beforeEach(() => {
testValidation = createFooValidation();
- responseFactory = {
- custom: jest.fn(({ body, statusCode }) => ({
- options: {},
- status: statusCode,
- payload: body,
- })),
- badRequest: jest.fn(({ body }) => ({ status: 400, payload: body, options: {} })),
- ok: jest.fn(({ body } = {}) => ({
- options: {},
- status: 200,
- payload: body,
- })),
- } as any;
router = createRouter();
+ versionedRouter = CoreVersionedRouter.from({
+ router,
+ log: loggingSystemMock.createLogger(),
+ });
});
afterEach(() => {
@@ -54,7 +42,6 @@ describe('Versioned route', () => {
describe('#getRoutes', () => {
it('returns the expected metadata', () => {
- const versionedRouter = CoreVersionedRouter.from({ router });
versionedRouter
.get({
path: '/test/{id}',
@@ -93,7 +80,6 @@ describe('Versioned route', () => {
});
it('can register multiple handlers', () => {
- const versionedRouter = CoreVersionedRouter.from({ router });
versionedRouter
.get({ path: '/test/{id}', access: 'internal' })
.addVersion({ version: '1', validate: false }, handlerFn)
@@ -104,11 +90,18 @@ describe('Versioned route', () => {
const [route] = routes;
expect(route.handlers).toHaveLength(3);
// We only register one route with the underlying router
- expect(router.get).toHaveBeenCalledTimes(1);
+ expect(router.registerRoute).toHaveBeenCalledTimes(1);
+ expect(router.registerRoute).toHaveBeenCalledWith({
+ isVersioned: true,
+ handler: expect.any(Function),
+ security: expect.any(Function),
+ method: 'get',
+ options: { access: 'internal' },
+ path: '/test/{id}',
+ });
});
it('does not allow specifying a handler for the same version more than once', () => {
- const versionedRouter = CoreVersionedRouter.from({ router });
expect(() =>
versionedRouter
.get({ path: '/test/{id}', access: 'internal' })
@@ -121,7 +114,6 @@ describe('Versioned route', () => {
});
it('only allows versions that are numbers greater than 0 for internal APIs', () => {
- const versionedRouter = CoreVersionedRouter.from({ router });
expect(() =>
versionedRouter
.get({ path: '/test/{id}', access: 'internal' })
@@ -145,7 +137,6 @@ describe('Versioned route', () => {
});
it('only allows correctly formatted version date strings for public APIs', () => {
- const versionedRouter = CoreVersionedRouter.from({ router });
expect(() =>
versionedRouter
.get({ path: '/test/{id}', access: 'public' })
@@ -168,8 +159,7 @@ describe('Versioned route', () => {
).not.toThrow();
});
- it('passes through the expected values to the IRouter registrar', () => {
- const versionedRouter = CoreVersionedRouter.from({ router });
+ it('passes through all expected values to the router registrar', () => {
const opts: Parameters[0] = {
path: '/test/{id}',
access: 'internal',
@@ -186,25 +176,29 @@ describe('Versioned route', () => {
};
versionedRouter.post(opts);
- expect(router.post).toHaveBeenCalledTimes(1);
- const { access, options } = opts;
-
- const expectedRouteConfig: RouteConfig = {
- path: opts.path,
- options: { access, ...options },
- validate: passThroughValidation,
- };
- expect(router.post).toHaveBeenCalledWith(
- expect.objectContaining(expectedRouteConfig),
- expect.any(Function),
- { isVersioned: true, events: false }
- );
+ expect(router.registerRoute).toHaveBeenCalledTimes(1);
+ expect(router.registerRoute).toHaveBeenCalledWith({
+ handler: expect.any(Function),
+ isVersioned: true,
+ method: 'post',
+ options: {
+ access: 'internal',
+ authRequired: true,
+ excludeFromOAS: true,
+ httpResource: true,
+ tags: ['access:test'],
+ timeout: { idleSocket: 10_000, payload: 60_000 },
+ xsrfRequired: false,
+ },
+ path: '/test/{id}',
+ security: expect.any(Function),
+ });
});
it('allows public versions other than "2023-10-31"', () => {
expect(() =>
- CoreVersionedRouter.from({ router, isDev: false })
+ CoreVersionedRouter.from({ router, log: loggingSystemMock.createLogger(), isDev: false })
.get({ access: 'public', path: '/foo' })
.addVersion({ version: '2023-01-31', validate: false }, (ctx, req, res) => res.ok())
).not.toThrow();
@@ -213,12 +207,11 @@ describe('Versioned route', () => {
it.each([['static' as const], ['lazy' as const]])(
'runs %s request validations',
async (staticOrLazy) => {
- let handler: RequestHandler;
+ let handler: InternalRouteHandler;
const { fooValidation, validateBodyFn, validateOutputFn, validateParamsFn, validateQueryFn } =
testValidation;
- (router.post as jest.Mock).mockImplementation((opts: unknown, fn) => (handler = fn));
- const versionedRouter = CoreVersionedRouter.from({ router });
+ (router.registerRoute as jest.Mock).mockImplementation((opts) => (handler = opts.handler));
versionedRouter.post({ path: '/test/{id}', access: 'internal' }).addVersion(
{
version: '1',
@@ -228,14 +221,12 @@ describe('Versioned route', () => {
);
const kibanaResponse = await handler!(
- {} as any,
createRequest({
version: '1',
body: { foo: 1 },
params: { foo: 1 },
query: { foo: 1 },
- }),
- responseFactory
+ })
);
expect(kibanaResponse.status).toBe(200);
@@ -247,7 +238,7 @@ describe('Versioned route', () => {
);
it('constructs lazily provided validations once (idempotency)', async () => {
- let handler: RequestHandler;
+ let handler: InternalRouteHandler;
const { fooValidation } = testValidation;
const response200 = fooValidation.response[200].body;
@@ -258,8 +249,7 @@ describe('Versioned route', () => {
const lazyResponse404 = jest.fn(() => response404());
fooValidation.response[404].body = lazyResponse404;
- (router.post as jest.Mock).mockImplementation((opts: unknown, fn) => (handler = fn));
- const versionedRouter = CoreVersionedRouter.from({ router });
+ (router.registerRoute as jest.Mock).mockImplementation((opts) => (handler = opts.handler));
const lazyValidation = jest.fn(() => fooValidation);
versionedRouter.post({ path: '/test/{id}', access: 'internal' }).addVersion(
{
@@ -271,14 +261,12 @@ describe('Versioned route', () => {
for (let i = 0; i < 10; i++) {
const { status } = await handler!(
- {} as any,
createRequest({
version: '1',
body: { foo: 1 },
params: { foo: 1 },
query: { foo: 1 },
- }),
- responseFactory
+ })
);
const [route] = versionedRouter.getRoutes();
const [
@@ -306,22 +294,28 @@ describe('Versioned route', () => {
});
describe('when in dev', () => {
+ beforeEach(() => {
+ versionedRouter = CoreVersionedRouter.from({
+ router,
+ isDev: true,
+ log: loggingSystemMock.createLogger(),
+ });
+ });
// NOTE: Temporary test to ensure single public API version is enforced
it('only allows "2023-10-31" as public route versions', () => {
expect(() =>
- CoreVersionedRouter.from({ router, isDev: true })
+ versionedRouter
.get({ access: 'public', path: '/foo' })
.addVersion({ version: '2023-01-31', validate: false }, (ctx, req, res) => res.ok())
).toThrow(/Invalid public version/);
});
it('runs response validations', async () => {
- let handler: RequestHandler;
+ let handler: InternalRouteHandler;
const { fooValidation, validateBodyFn, validateOutputFn, validateParamsFn, validateQueryFn } =
testValidation;
- (router.post as jest.Mock).mockImplementation((opts: unknown, fn) => (handler = fn));
- const versionedRouter = CoreVersionedRouter.from({ router, isDev: true });
+ (router.registerRoute as jest.Mock).mockImplementation((opts) => (handler = opts.handler));
versionedRouter.post({ path: '/test/{id}', access: 'internal' }).addVersion(
{
version: '1',
@@ -331,14 +325,12 @@ describe('Versioned route', () => {
);
const kibanaResponse = await handler!(
- {} as any,
createRequest({
version: '1',
body: { foo: 1 },
params: { foo: 1 },
query: { foo: 1 },
- }),
- responseFactory
+ })
);
expect(kibanaResponse.status).toBe(200);
@@ -349,10 +341,14 @@ describe('Versioned route', () => {
});
it('handles "undefined" response schemas', async () => {
- let handler: RequestHandler;
+ let handler: InternalRouteHandler;
- (router.post as jest.Mock).mockImplementation((opts: unknown, fn) => (handler = fn));
- const versionedRouter = CoreVersionedRouter.from({ router, isDev: true });
+ (router.registerRoute as jest.Mock).mockImplementation((opts) => (handler = opts.handler));
+ versionedRouter = CoreVersionedRouter.from({
+ router,
+ isDev: true,
+ log: loggingSystemMock.createLogger(),
+ });
versionedRouter.post({ path: '/test/{id}', access: 'internal' }).addVersion(
{
version: '1',
@@ -363,27 +359,29 @@ describe('Versioned route', () => {
await expect(
handler!(
- {} as any,
createRequest({
version: '1',
body: { foo: 1 },
params: { foo: 1 },
query: { foo: 1 },
- }),
- responseFactory
+ })
)
).resolves.not.toThrow();
});
it('runs custom response validations', async () => {
- let handler: RequestHandler;
+ let handler: InternalRouteHandler;
const { fooValidation, validateBodyFn, validateOutputFn, validateParamsFn, validateQueryFn } =
testValidation;
const custom = jest.fn(() => ({ value: 1 }));
fooValidation.response[200].body = { custom } as any;
- (router.post as jest.Mock).mockImplementation((opts: unknown, fn) => (handler = fn));
- const versionedRouter = CoreVersionedRouter.from({ router, isDev: true });
+ (router.registerRoute as jest.Mock).mockImplementation((opts) => (handler = opts.handler));
+ versionedRouter = CoreVersionedRouter.from({
+ router,
+ isDev: true,
+ log: loggingSystemMock.createLogger(),
+ });
versionedRouter.post({ path: '/test/{id}', access: 'internal' }).addVersion(
{
version: '1',
@@ -393,14 +391,12 @@ describe('Versioned route', () => {
);
const kibanaResponse = await handler!(
- {} as any,
createRequest({
version: '1',
body: { foo: 1 },
params: { foo: 1 },
query: { foo: 1 },
- }),
- responseFactory
+ })
);
expect(kibanaResponse.status).toBe(200);
@@ -413,15 +409,16 @@ describe('Versioned route', () => {
});
it('allows using default resolution for specific internal routes', async () => {
- const versionedRouter = CoreVersionedRouter.from({
+ versionedRouter = CoreVersionedRouter.from({
router,
isDev: true,
+ log: loggingSystemMock.createLogger(),
useVersionResolutionStrategyForInternalPaths: ['/bypass_me/{id?}'],
});
- let bypassVersionHandler: RequestHandler;
- (router.post as jest.Mock).mockImplementation(
- (opts: unknown, fn) => (bypassVersionHandler = fn)
+ let bypassVersionHandler: InternalRouteHandler;
+ (router.registerRoute as jest.Mock).mockImplementation(
+ (opts) => (bypassVersionHandler = opts.handler)
);
versionedRouter.post({ path: '/bypass_me/{id?}', access: 'internal' }).addVersion(
{
@@ -431,8 +428,10 @@ describe('Versioned route', () => {
handlerFn
);
- let doNotBypassHandler1: RequestHandler;
- (router.put as jest.Mock).mockImplementation((opts: unknown, fn) => (doNotBypassHandler1 = fn));
+ let doNotBypassHandler1: InternalRouteHandler;
+ (router.registerRoute as jest.Mock).mockImplementation(
+ (opts) => (doNotBypassHandler1 = opts.handler)
+ );
versionedRouter.put({ path: '/do_not_bypass_me/{id}', access: 'internal' }).addVersion(
{
version: '1',
@@ -441,8 +440,10 @@ describe('Versioned route', () => {
handlerFn
);
- let doNotBypassHandler2: RequestHandler;
- (router.get as jest.Mock).mockImplementation((opts: unknown, fn) => (doNotBypassHandler2 = fn));
+ let doNotBypassHandler2: InternalRouteHandler;
+ (router.registerRoute as jest.Mock).mockImplementation(
+ (opts) => (doNotBypassHandler2 = opts.handler)
+ );
versionedRouter.get({ path: '/do_not_bypass_me_either', access: 'internal' }).addVersion(
{
version: '1',
@@ -452,22 +453,12 @@ describe('Versioned route', () => {
);
const byPassedVersionResponse = await bypassVersionHandler!(
- {} as any,
- createRequest({ version: undefined }),
- responseFactory
+ createRequest({ version: undefined })
);
- const doNotBypassResponse1 = await doNotBypassHandler1!(
- {} as any,
- createRequest({ version: undefined }),
- responseFactory
- );
+ const doNotBypassResponse1 = await doNotBypassHandler1!(createRequest({ version: undefined }));
- const doNotBypassResponse2 = await doNotBypassHandler2!(
- {} as any,
- createRequest({ version: undefined }),
- responseFactory
- );
+ const doNotBypassResponse2 = await doNotBypassHandler2!(createRequest({ version: undefined }));
expect(byPassedVersionResponse.status).toBe(200);
expect(doNotBypassResponse1.status).toBe(400);
@@ -477,7 +468,6 @@ describe('Versioned route', () => {
});
it('can register multiple handlers with different security configurations', () => {
- const versionedRouter = CoreVersionedRouter.from({ router });
const securityConfig1: RouteSecurity = {
authz: {
requiredPrivileges: ['foo'],
@@ -533,11 +523,10 @@ describe('Versioned route', () => {
expect(route.handlers[0].options.security).toStrictEqual(securityConfig1);
expect(route.handlers[1].options.security).toStrictEqual(securityConfig2);
expect(route.handlers[2].options.security).toStrictEqual(securityConfig3);
- expect(router.get).toHaveBeenCalledTimes(1);
+ expect(router.registerRoute).toHaveBeenCalledTimes(1);
});
it('falls back to default security configuration if it is not specified for specific version', () => {
- const versionedRouter = CoreVersionedRouter.from({ router });
const securityConfigDefault: RouteSecurity = {
authz: {
requiredPrivileges: ['foo', 'bar', 'baz'],
@@ -613,11 +602,10 @@ describe('Versioned route', () => {
headers: { [ELASTIC_HTTP_VERSION_HEADER]: '99' },
})
).toStrictEqual(securityConfigDefault);
- expect(router.get).toHaveBeenCalledTimes(1);
+ expect(router.registerRoute).toHaveBeenCalledTimes(1);
});
it('validates security configuration', () => {
- const versionedRouter = CoreVersionedRouter.from({ router });
const validSecurityConfig: RouteSecurity = {
authz: {
requiredPrivileges: ['foo'],
@@ -668,7 +656,6 @@ describe('Versioned route', () => {
});
it('should correctly merge security configuration for versions', () => {
- const versionedRouter = CoreVersionedRouter.from({ router });
const validSecurityConfig: RouteSecurity = {
authz: {
requiredPrivileges: ['foo'],
@@ -704,4 +691,99 @@ describe('Versioned route', () => {
expect(security.authz).toEqual({ requiredPrivileges: ['foo', 'bar'] });
});
+
+ describe('emits post validation events on the router', () => {
+ let handler: InternalRouteHandler;
+
+ it('for routes with validation', async () => {
+ const { fooValidation } = testValidation;
+ (router.registerRoute as jest.Mock).mockImplementation((opts) => (handler = opts.handler));
+ versionedRouter.post({ path: '/test/{id}', access: 'internal' }).addVersion(
+ {
+ version: '1',
+ validate: fooValidation,
+ options: {
+ deprecated: {
+ severity: 'warning',
+ reason: { type: 'bump', newApiVersion: '123' },
+ documentationUrl: 'http://test.foo',
+ },
+ },
+ },
+ handlerFn
+ );
+
+ await handler!(
+ createRequest({
+ version: '1',
+ body: { foo: 1 },
+ params: { foo: 1 },
+ query: { foo: 1 },
+ })
+ );
+ // Failed validation
+ await handler!(createRequest({ version: '1' }));
+
+ expect(router.emitPostValidate).toHaveBeenCalledTimes(2);
+ expect(router.emitPostValidate).toHaveBeenNthCalledWith(
+ 1,
+ expect.objectContaining({ apiVersion: '1' }),
+ {
+ deprecated: {
+ severity: 'warning',
+ reason: { type: 'bump', newApiVersion: '123' },
+ documentationUrl: 'http://test.foo',
+ },
+ isInternalApiRequest: false,
+ isPublicAccess: false,
+ }
+ );
+ expect(router.emitPostValidate).toHaveBeenNthCalledWith(
+ 2,
+ expect.objectContaining({ apiVersion: '1' }),
+ {
+ deprecated: {
+ severity: 'warning',
+ reason: { type: 'bump', newApiVersion: '123' },
+ documentationUrl: 'http://test.foo',
+ },
+ isInternalApiRequest: false,
+ isPublicAccess: false,
+ }
+ );
+ });
+
+ it('for routes without validation', async () => {
+ (router.registerRoute as jest.Mock).mockImplementation((opts) => (handler = opts.handler));
+ versionedRouter.post({ path: '/test/{id}', access: 'internal' }).addVersion(
+ {
+ version: '1',
+ validate: false,
+ options: {
+ deprecated: {
+ severity: 'warning',
+ reason: { type: 'bump', newApiVersion: '123' },
+ documentationUrl: 'http://test.foo',
+ },
+ },
+ },
+ handlerFn
+ );
+
+ await handler!(createRequest({ version: '1' }));
+ expect(router.emitPostValidate).toHaveBeenCalledTimes(1);
+ expect(router.emitPostValidate).toHaveBeenCalledWith(
+ expect.objectContaining({ apiVersion: '1' }),
+ {
+ deprecated: {
+ severity: 'warning',
+ reason: { type: 'bump', newApiVersion: '123' },
+ documentationUrl: 'http://test.foo',
+ },
+ isInternalApiRequest: false,
+ isPublicAccess: false,
+ }
+ );
+ });
+ });
});
diff --git a/src/core/packages/http/router-server-internal/src/versioned_router/core_versioned_route.test.util.ts b/src/core/packages/http/router-server-internal/src/versioned_router/core_versioned_route.test.util.ts
index c7c8d30666990..58f29442e94da 100644
--- a/src/core/packages/http/router-server-internal/src/versioned_router/core_versioned_route.test.util.ts
+++ b/src/core/packages/http/router-server-internal/src/versioned_router/core_versioned_route.test.util.ts
@@ -10,8 +10,6 @@
// eslint-disable-next-line @kbn/imports/no_boundary_crossing
import { hapiMocks } from '@kbn/hapi-mocks';
import { ApiVersion, ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
-import { CoreKibanaRequest } from '../request';
-import { passThroughValidation } from './core_versioned_route';
export function createRequest(
{
@@ -19,18 +17,15 @@ export function createRequest(
body,
params,
query,
- }: { version: undefined | ApiVersion; body?: object; params?: object; query?: object } = {
+ }: { version?: undefined | ApiVersion; body?: object; params?: object; query?: object } = {
version: '1',
}
) {
- return CoreKibanaRequest.from(
- hapiMocks.createRequest({
- payload: body,
- params,
- query,
- headers: { [ELASTIC_HTTP_VERSION_HEADER]: version },
- app: { requestId: 'fakeId' },
- }),
- passThroughValidation
- );
+ return hapiMocks.createRequest({
+ payload: body,
+ params,
+ query,
+ headers: { [ELASTIC_HTTP_VERSION_HEADER]: version },
+ app: { requestId: 'fakeId' },
+ });
}
diff --git a/src/core/packages/http/router-server-internal/src/versioned_router/core_versioned_route.ts b/src/core/packages/http/router-server-internal/src/versioned_router/core_versioned_route.ts
index 49e386f73e675..04560c3e76e5a 100644
--- a/src/core/packages/http/router-server-internal/src/versioned_router/core_versioned_route.ts
+++ b/src/core/packages/http/router-server-internal/src/versioned_router/core_versioned_route.ts
@@ -7,16 +7,12 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-import { schema } from '@kbn/config-schema';
import {
ELASTIC_HTTP_VERSION_HEADER,
ELASTIC_HTTP_VERSION_QUERY_PARAM,
} from '@kbn/core-http-common';
import type {
RequestHandler,
- RequestHandlerContextBase,
- KibanaRequest,
- KibanaResponseFactory,
ApiVersion,
VersionedRoute,
VersionedRouteConfig,
@@ -26,12 +22,11 @@ import type {
RouteSecurity,
RouteMethod,
VersionedRouterRoute,
- PostValidationMetadata,
} from '@kbn/core-http-server';
-import type { Mutable } from 'utility-types';
+import { Request } from '@hapi/hapi';
+import { Logger } from '@kbn/logging';
import type { HandlerResolutionStrategy, Method, Options } from './types';
-import { validate } from './validate';
import {
isAllowedPublicVersion,
isValidRouteVersion,
@@ -39,13 +34,16 @@ import {
readVersion,
removeQueryVersion,
} from './route_version_utils';
-import { getVersionHeader, injectVersionHeader } from '../util';
+import { injectVersionHeader } from '../util';
import { validRouteSecurity } from '../security_route_config_validator';
import { resolvers } from './handler_resolvers';
import { prepareVersionedRouteValidation, unwrapVersionedResponseBodyValidation } from './util';
import type { RequestLike } from './route_version_utils';
-import { Router } from '../router';
+import { RequestHandlerEnhanced, Router } from '../router';
+import { kibanaResponseFactory as responseFactory } from '../response';
+import { validateHapiRequest } from '../route';
+import { RouteValidator } from '../validator';
interface InternalVersionedRouteConfig extends VersionedRouteConfig {
isDev: boolean;
@@ -53,13 +51,6 @@ interface InternalVersionedRouteConfig extends VersionedR
defaultHandlerResolutionStrategy: HandlerResolutionStrategy;
}
-// This validation is a pass-through so that we can apply our version-specific validation later
-export const passThroughValidation = {
- body: schema.nullable(schema.any()),
- params: schema.nullable(schema.any()),
- query: schema.nullable(schema.any()),
-};
-
function extractValidationSchemaFromHandler(handler: VersionedRouterRoute['handlers'][0]) {
if (handler.options.validate === false) return undefined;
if (typeof handler.options.validate === 'function') return handler.options.validate();
@@ -70,23 +61,25 @@ export class CoreVersionedRoute implements VersionedRoute {
public readonly handlers = new Map<
ApiVersion,
{
- fn: RequestHandler;
+ fn: RequestHandlerEnhanced;
options: Options;
}
>();
public static from({
router,
+ log,
method,
path,
options,
}: {
router: Router;
+ log: Logger;
method: Method;
path: string;
options: InternalVersionedRouteConfig;
}) {
- return new CoreVersionedRoute(router, method, path, options);
+ return new CoreVersionedRoute(router, log, method, path, options);
}
public readonly options: VersionedRouteConfig;
@@ -99,6 +92,7 @@ export class CoreVersionedRoute implements VersionedRoute {
private defaultHandlerResolutionStrategy: HandlerResolutionStrategy;
private constructor(
private readonly router: Router,
+ private readonly log: Logger,
public readonly method: Method,
public readonly path: string,
internalOptions: InternalVersionedRouteConfig
@@ -117,17 +111,14 @@ export class CoreVersionedRoute implements VersionedRoute {
this.enableQueryVersion = options.enableQueryVersion === true;
this.defaultSecurityConfig = validRouteSecurity(options.security, options.options);
this.options = options;
- this.router[this.method](
- {
- path: this.path,
- validate: passThroughValidation,
- // @ts-expect-error upgrade typescript v5.1.6
- options: this.getRouteConfigOptions(),
- security: this.getSecurity,
- },
- this.requestHandler,
- { isVersioned: true, events: false }
- );
+ this.router.registerRoute({
+ path: this.path,
+ options: this.getRouteConfigOptions(),
+ security: this.getSecurity,
+ handler: (request) => this.handle(request),
+ isVersioned: true,
+ method: this.method,
+ });
}
private getRouteConfigOptions(): RouteConfigOptions {
@@ -167,94 +158,71 @@ export class CoreVersionedRoute implements VersionedRoute {
return version;
}
- private requestHandler = async (
- ctx: RequestHandlerContextBase,
- originalReq: KibanaRequest,
- res: KibanaResponseFactory
- ): Promise => {
+ private handle = async (hapiRequest: Request): Promise => {
if (this.handlers.size <= 0) {
- return res.custom({
+ return responseFactory.custom({
statusCode: 500,
body: `No handlers registered for [${this.method}] [${this.path}].`,
});
}
- const req = originalReq as Mutable;
- const version = this.getVersion(req);
- req.apiVersion = version;
+ const version = this.getVersion(hapiRequest);
if (!version) {
- return res.badRequest({
+ return responseFactory.badRequest({
body: `Please specify a version via ${ELASTIC_HTTP_VERSION_HEADER} header. Available versions: ${this.versionsToString()}`,
});
}
- if (hasQueryVersion(req)) {
- if (this.enableQueryVersion) {
- // This endpoint has opted-in to query versioning, so we remove the query parameter as it is reserved
- removeQueryVersion(req);
- } else
- return res.badRequest({
+ if (hasQueryVersion(hapiRequest)) {
+ if (!this.enableQueryVersion) {
+ return responseFactory.badRequest({
body: `Use of query parameter "${ELASTIC_HTTP_VERSION_QUERY_PARAM}" is not allowed. Please specify the API version using the "${ELASTIC_HTTP_VERSION_HEADER}" header.`,
});
+ }
+ removeQueryVersion(hapiRequest);
}
const invalidVersionMessage = isValidRouteVersion(this.isPublic, version);
if (invalidVersionMessage) {
- return res.badRequest({ body: invalidVersionMessage });
+ return responseFactory.badRequest({ body: invalidVersionMessage });
}
const handler = this.handlers.get(version);
if (!handler) {
- return res.badRequest({
+ return responseFactory.badRequest({
body: `No version "${version}" available for [${this.method}] [${
this.path
}]. Available versions are: ${this.versionsToString()}`,
});
}
const validation = extractValidationSchemaFromHandler(handler);
- const postValidateMetadata: PostValidationMetadata = {
- deprecated: handler.options.options?.deprecated,
- isInternalApiRequest: req.isInternalApiRequest,
- isPublicAccess: this.isPublic,
- };
- if (
- validation?.request &&
- Boolean(validation.request.body || validation.request.params || validation.request.query)
- ) {
- try {
- const { body, params, query } = validate(req, validation.request);
- req.body = body;
- req.params = params;
- req.query = query;
- } catch (e) {
- // Emit onPostValidation even if validation fails.
-
- this.router.emitPostValidate(req, postValidateMetadata);
- return res.badRequest({ body: e.message, headers: getVersionHeader(version) });
- }
- } else {
- // Preserve behavior of not passing through unvalidated data
- req.body = {};
- req.params = {};
- req.query = {};
+ const { error, ok: kibanaRequest } = validateHapiRequest(hapiRequest, {
+ routeInfo: {
+ access: this.options.access,
+ httpResource: this.options.options?.httpResource,
+ deprecated: handler.options?.options?.deprecated,
+ },
+ router: this.router,
+ log: this.log,
+ routeSchemas: validation?.request ? RouteValidator.from(validation.request) : undefined,
+ version,
+ });
+ if (error) {
+ return injectVersionHeader(version, error);
}
- this.router.emitPostValidate(req, postValidateMetadata);
-
- const response = await handler.fn(ctx, req, res);
+ const response = await handler.fn(kibanaRequest, responseFactory);
if (this.isDev && validation?.response?.[response.status]?.body) {
const { [response.status]: responseValidation, unsafe } = validation.response;
try {
- validate(
- { body: response.payload },
- {
- body: unwrapVersionedResponseBodyValidation(responseValidation.body!),
- unsafe: { body: unsafe?.body },
- }
- );
+ const validator = RouteValidator.from({
+ body: unwrapVersionedResponseBodyValidation(responseValidation.body!),
+ unsafe: { body: unsafe?.body },
+ });
+ validator.getBody(response.payload, 'response body');
} catch (e) {
- return res.custom({
+ return responseFactory.custom({
statusCode: 500,
body: `Failed output validation: ${e.message}`,
});
@@ -292,13 +260,16 @@ export class CoreVersionedRoute implements VersionedRoute {
this.validateVersion(options.version);
options = prepareVersionedRouteValidation(options);
this.handlers.set(options.version, {
- fn: handler,
+ fn: this.router.enhanceWithContext(handler),
options,
});
return this;
}
- public getHandlers(): Array<{ fn: RequestHandler; options: Options }> {
+ public getHandlers(): Array<{
+ fn: RequestHandlerEnhanced;
+ options: Options;
+ }> {
return [...this.handlers.values()];
}
diff --git a/src/core/packages/http/router-server-internal/src/versioned_router/core_versioned_router.test.ts b/src/core/packages/http/router-server-internal/src/versioned_router/core_versioned_router.test.ts
index a3ffffc0ef219..7b9fbbf938807 100644
--- a/src/core/packages/http/router-server-internal/src/versioned_router/core_versioned_router.test.ts
+++ b/src/core/packages/http/router-server-internal/src/versioned_router/core_versioned_router.test.ts
@@ -7,18 +7,24 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
+import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
import { Router } from '../router';
import { CoreVersionedRouter } from '.';
import { createRouter } from './mocks';
+const pluginId = Symbol('test');
describe('Versioned router', () => {
let router: Router;
+ let versionedRouter: CoreVersionedRouter;
beforeEach(() => {
- router = createRouter();
+ router = createRouter({ pluginId });
+ versionedRouter = CoreVersionedRouter.from({
+ router,
+ log: loggingSystemMock.createLogger(),
+ });
});
it('can register multiple routes', () => {
- const versionedRouter = CoreVersionedRouter.from({ router });
versionedRouter.get({ path: '/test/{id}', access: 'internal' });
versionedRouter.post({ path: '/test', access: 'internal' });
versionedRouter.delete({ path: '/test', access: 'internal' });
@@ -26,13 +32,10 @@ describe('Versioned router', () => {
});
it('registers pluginId if router has one', () => {
- const pluginId = Symbol('test');
- const versionedRouter = CoreVersionedRouter.from({ router: createRouter({ pluginId }) });
expect(versionedRouter.pluginId).toBe(pluginId);
});
it('provides the expected metadata', () => {
- const versionedRouter = CoreVersionedRouter.from({ router });
versionedRouter.get({
path: '/test/{id}',
access: 'internal',
diff --git a/src/core/packages/http/router-server-internal/src/versioned_router/core_versioned_router.ts b/src/core/packages/http/router-server-internal/src/versioned_router/core_versioned_router.ts
index ef1f8255420ae..0570ac3cf099c 100644
--- a/src/core/packages/http/router-server-internal/src/versioned_router/core_versioned_router.ts
+++ b/src/core/packages/http/router-server-internal/src/versioned_router/core_versioned_router.ts
@@ -14,13 +14,16 @@ import type {
VersionedRouterRoute,
} from '@kbn/core-http-server';
import { omit } from 'lodash';
+import { Logger } from '@kbn/logging';
import { CoreVersionedRoute } from './core_versioned_route';
import type { HandlerResolutionStrategy, Method } from './types';
-import { getRouteFullPath, type Router } from '../router';
+import type { Router } from '../router';
+import { getRouteFullPath } from '../util';
/** @internal */
export interface VersionedRouterArgs {
router: Router;
+ log: Logger;
/**
* Which route resolution algo to use.
* @note default to "oldest", but when running in dev default to "none"
@@ -52,12 +55,14 @@ export class CoreVersionedRouter implements VersionedRouter {
public pluginId?: symbol;
public static from({
router,
+ log,
defaultHandlerResolutionStrategy,
isDev,
useVersionResolutionStrategyForInternalPaths,
}: VersionedRouterArgs) {
return new CoreVersionedRouter(
router,
+ log,
defaultHandlerResolutionStrategy,
isDev,
useVersionResolutionStrategyForInternalPaths
@@ -65,6 +70,7 @@ export class CoreVersionedRouter implements VersionedRouter {
}
private constructor(
public readonly router: Router,
+ private readonly log: Logger,
public readonly defaultHandlerResolutionStrategy: HandlerResolutionStrategy = 'oldest',
public readonly isDev: boolean = false,
useVersionResolutionStrategyForInternalPaths: string[] = []
@@ -80,6 +86,7 @@ export class CoreVersionedRouter implements VersionedRouter {
(options: VersionedRouteConfig): VersionedRoute => {
const route = CoreVersionedRoute.from({
router: this.router,
+ log: this.log,
method: routeMethod,
path: options.path,
options: {
diff --git a/src/core/packages/http/router-server-internal/src/versioned_router/mocks.ts b/src/core/packages/http/router-server-internal/src/versioned_router/mocks.ts
index 36a672ca6a9f7..88e719b5033dc 100644
--- a/src/core/packages/http/router-server-internal/src/versioned_router/mocks.ts
+++ b/src/core/packages/http/router-server-internal/src/versioned_router/mocks.ts
@@ -21,6 +21,8 @@ export function createRouter(opts: CreateMockRouterOptions = {}) {
getRoutes: jest.fn(),
handleLegacyErrors: jest.fn(),
emitPostValidate: jest.fn(),
+ registerRoute: jest.fn(),
+ enhanceWithContext: jest.fn((fn) => fn.bind(null, {})),
patch: jest.fn(),
routerPath: '',
versioned: {} as any,
diff --git a/src/core/packages/http/router-server-internal/src/versioned_router/route_version_utils.test.ts b/src/core/packages/http/router-server-internal/src/versioned_router/route_version_utils.test.ts
index da1e63c9ccca3..249867d2417c0 100644
--- a/src/core/packages/http/router-server-internal/src/versioned_router/route_version_utils.test.ts
+++ b/src/core/packages/http/router-server-internal/src/versioned_router/route_version_utils.test.ts
@@ -7,10 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-import { KibanaRequest } from '@kbn/core-http-server';
import { hapiMocks } from '@kbn/hapi-mocks';
-import { CoreKibanaRequest } from '../request';
-import { passThroughValidation } from './core_versioned_route';
import {
isValidRouteVersion,
isAllowedPublicVersion,
@@ -65,9 +62,8 @@ describe('isValidRouteVersion', () => {
});
});
-function getRequest(arg: { headers?: any; query?: any } = {}): KibanaRequest {
- const request = hapiMocks.createRequest({ ...arg });
- return CoreKibanaRequest.from(request, passThroughValidation);
+function getRequest(arg: { headers?: any; query?: any } = {}) {
+ return hapiMocks.createRequest({ ...arg });
}
describe('readVersion', () => {
diff --git a/src/core/packages/http/router-server-internal/src/versioned_router/route_version_utils.ts b/src/core/packages/http/router-server-internal/src/versioned_router/route_version_utils.ts
index 2b82dcc12acd2..e6151148928d2 100644
--- a/src/core/packages/http/router-server-internal/src/versioned_router/route_version_utils.ts
+++ b/src/core/packages/http/router-server-internal/src/versioned_router/route_version_utils.ts
@@ -60,12 +60,14 @@ export interface RequestLike {
}
export function hasQueryVersion(
- request: Mutable
+ request: RequestLike
): request is Mutable {
return isObject(request.query) && ELASTIC_HTTP_VERSION_QUERY_PARAM in request.query;
}
-export function removeQueryVersion(request: Mutable): void {
- delete request.query[ELASTIC_HTTP_VERSION_QUERY_PARAM];
+export function removeQueryVersion(request: RequestLike): void {
+ if (request.query) {
+ delete (request.query as { [key: string]: string })[ELASTIC_HTTP_VERSION_QUERY_PARAM];
+ }
}
function readQueryVersion(request: RequestLike): undefined | ApiVersion {
diff --git a/src/core/packages/http/router-server-internal/src/versioned_router/validate.ts b/src/core/packages/http/router-server-internal/src/versioned_router/validate.ts
deleted file mode 100644
index e1ed81a4ca2ac..0000000000000
--- a/src/core/packages/http/router-server-internal/src/versioned_router/validate.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
- * Public License v 1"; you may not use this file except in compliance with, at
- * your election, the "Elastic License 2.0", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-import type { RouteValidatorFullConfigRequest } from '@kbn/core-http-server';
-import { RouteValidator } from '../validator';
-
-/** Will throw if any of the validation checks fail */
-export function validate(
- data: { body?: unknown; params?: unknown; query?: unknown },
- runtimeSchema: RouteValidatorFullConfigRequest
-): { body: unknown; params: unknown; query: unknown } {
- const validator = RouteValidator.from(runtimeSchema);
- return {
- params: validator.getParams(data.params, 'request params'),
- query: validator.getQuery(data.query, 'request query'),
- body: validator.getBody(data.body, 'request body'),
- };
-}
diff --git a/src/core/packages/http/server-internal/src/http_server.ts b/src/core/packages/http/server-internal/src/http_server.ts
index 06b14911fd9bf..9f04230a0844b 100644
--- a/src/core/packages/http/server-internal/src/http_server.ts
+++ b/src/core/packages/http/server-internal/src/http_server.ts
@@ -37,6 +37,7 @@ import type {
IAuthHeadersStorage,
RouterDeprecatedApiDetails,
RouteMethod,
+ VersionedRouterRoute,
} from '@kbn/core-http-server';
import { performance } from 'perf_hooks';
import { isBoom } from '@hapi/boom';
@@ -406,10 +407,12 @@ export class HttpServer {
.map((route) => {
const access = route.options.access;
if (route.isVersioned === true) {
- return [...route.handlers.entries()].map(([_, { options }]) => {
- const deprecated = options.options?.deprecated;
- return { route, version: `${options.version}`, deprecated, access };
- });
+ return [...(route as VersionedRouterRoute).handlers.entries()].map(
+ ([_, { options }]) => {
+ const deprecated = options.options?.deprecated;
+ return { route, version: `${options.version}`, deprecated, access };
+ }
+ );
}
return { route, version: undefined, deprecated: route.options.deprecated, access };
diff --git a/src/core/packages/http/server/index.ts b/src/core/packages/http/server/index.ts
index 7b79dfe313bd6..d0e9475cb1899 100644
--- a/src/core/packages/http/server/index.ts
+++ b/src/core/packages/http/server/index.ts
@@ -124,6 +124,7 @@ export type {
InternalRouteSecurity,
RouteDeprecationInfo,
PostValidationMetadata,
+ AnyKibanaRequest,
} from './src/router';
export {
validBodyOutput,
diff --git a/src/core/packages/http/server/src/router/index.ts b/src/core/packages/http/server/src/router/index.ts
index 166fcad324953..278ab761cf8d2 100644
--- a/src/core/packages/http/server/src/router/index.ts
+++ b/src/core/packages/http/server/src/router/index.ts
@@ -31,6 +31,7 @@ export type {
KibanaRouteOptions,
RouteSecurityGetter,
InternalRouteSecurity,
+ AnyKibanaRequest,
} from './request';
export type { RequestHandlerWrapper, RequestHandler } from './request_handler';
export type { RequestHandlerContextBase } from './request_handler_context';
diff --git a/src/core/packages/http/server/src/router/request.ts b/src/core/packages/http/server/src/router/request.ts
index 0a55cf022ab15..5019cce51c80d 100644
--- a/src/core/packages/http/server/src/router/request.ts
+++ b/src/core/packages/http/server/src/router/request.ts
@@ -214,3 +214,8 @@ export interface KibanaRequest<
*/
readonly body: Body;
}
+
+/**
+ * @remark Convenience type, use when the concrete values of P, Q, B and route method do not matter.
+ */
+export type AnyKibanaRequest = KibanaRequest;
diff --git a/src/core/packages/http/server/src/router/router.ts b/src/core/packages/http/server/src/router/router.ts
index f770cfcc45e4b..f6a039a4130a9 100644
--- a/src/core/packages/http/server/src/router/router.ts
+++ b/src/core/packages/http/server/src/router/router.ts
@@ -139,7 +139,7 @@ export interface RouterRoute {
req: Request,
responseToolkit: ResponseToolkit
) => Promise>;
- isVersioned: false;
+ isVersioned: boolean;
}
/** @public */
diff --git a/src/core/packages/http/server/src/versioning/types.ts b/src/core/packages/http/server/src/versioning/types.ts
index 9b3480c554733..69f4c77d86c90 100644
--- a/src/core/packages/http/server/src/versioning/types.ts
+++ b/src/core/packages/http/server/src/versioning/types.ts
@@ -370,6 +370,6 @@ export interface VersionedRouterRoute {
method: string;
path: string;
options: Omit, 'path'>;
- handlers: Array<{ fn: RequestHandler; options: AddVersionOpts
}>;
+ handlers: Array<{ fn: Function; options: AddVersionOpts
}>;
isVersioned: true;
}