[core.http] Add warning header to deprecated endpoints (#205926)

## Summary

resolves https://github.com/elastic/kibana/issues/105692

This PR adds a pre response handler that sets a warning header if the
requested endpoint is deprecated.

### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

### Identify risks

Does this PR introduce any risks? For example, consider risks like hard
to test bugs, performance regression, potential of data loss.

Describe the risk, its severity, and mitigation for each identified
risk. Invite stakeholders and evaluate how to proceed before merging.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Jesus Wahrman 2025-01-20 13:40:53 +01:00 committed by GitHub
parent 39119b553e
commit 0f67c78659
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 427 additions and 39 deletions

View file

@ -21,3 +21,4 @@ export { isKibanaRequest, isRealRequest, ensureRawRequest, CoreKibanaRequest } f
export { isSafeMethod } from './src/route';
export { HapiResponseAdapter } from './src/response_adapter';
export { kibanaResponseFactory, lifecycleResponseFactory, KibanaResponse } from './src/response';
export { getWarningHeaderMessageFromRouteDeprecation } from './src/get_warning_header_message';

View file

@ -0,0 +1,41 @@
/*
* 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 { RouteDeprecationInfo } from '@kbn/core-http-server';
import { getWarningHeaderMessageFromRouteDeprecation } from './get_warning_header_message';
describe('getWarningHeaderMessageFromRouteDeprecation', () => {
it('creates the warning with a default message if the deprecation object does not have one', () => {
const kibanaVersion = '12.31.45';
const expectedMessage = `299 Kibana-${kibanaVersion} "This endpoint deprecated"`;
const deprecationObject: RouteDeprecationInfo = {
reason: { type: 'deprecate' },
severity: 'warning',
documentationUrl: 'fakeurl.com',
};
expect(getWarningHeaderMessageFromRouteDeprecation(deprecationObject, expectedMessage)).toMatch(
expectedMessage
);
});
it('creates the warning with the deprecation object message', () => {
const kibanaVersion = '12.31.45';
const msg = 'Custom deprecation message for this object';
const expectedMessage = `299 Kibana-${kibanaVersion} "${msg}"`;
const deprecationObject: RouteDeprecationInfo = {
reason: { type: 'deprecate' },
severity: 'warning',
documentationUrl: 'fakeurl.com',
message: msg,
};
expect(getWarningHeaderMessageFromRouteDeprecation(deprecationObject, expectedMessage)).toMatch(
expectedMessage
);
});
});

View file

@ -0,0 +1,19 @@
/*
* 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 { RouteDeprecationInfo } from '@kbn/core-http-server';
export function getWarningHeaderMessageFromRouteDeprecation(
deprecationObject: RouteDeprecationInfo,
kibanaVersion: string
): string {
const msg = deprecationObject.message ?? 'This endpoint is deprecated';
const warningMessage = `299 Kibana-${kibanaVersion} "${msg}"`;
return warningMessage;
}

View file

@ -14,6 +14,7 @@ import { createRequestMock } from '@kbn/hapi-mocks/src/request';
import { createFooValidation } from './router.test.util';
import { Router, type RouterOptions } from './router';
import type { RouteValidatorRequestAndResponses } from '@kbn/core-http-server';
import { getEnvOptions, createTestEnv } from '@kbn/config-mocks';
const mockResponse = {
code: jest.fn().mockImplementation(() => mockResponse),
@ -26,9 +27,12 @@ const mockResponseToolkit = {
const logger = loggingSystemMock.create().get();
const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {});
const options = getEnvOptions();
options.cliArgs.dev = false;
const env = createTestEnv({ envOptions: options });
const routerOptions: RouterOptions = {
isDev: false,
env,
versionedRouterOptions: {
defaultHandlerResolutionStrategy: 'oldest',
useVersionResolutionStrategyForInternalPaths: [],
@ -273,7 +277,7 @@ describe('Router', () => {
it('registers pluginId if provided', () => {
const pluginId = Symbol('test');
const router = new Router('', logger, enhanceWithContext, { pluginId });
const router = new Router('', logger, enhanceWithContext, { pluginId, env });
expect(router.pluginId).toBe(pluginId);
});

View file

@ -30,6 +30,7 @@ import type {
IKibanaResponse,
} from '@kbn/core-http-server';
import type { RouteSecurityGetter } from '@kbn/core-http-server';
import { Env } from '@kbn/config';
import { CoreVersionedRouter } from './versioned_router';
import { CoreKibanaRequest, getProtocolFromRequest } from './request';
import { kibanaResponseFactory } from './response';
@ -72,8 +73,7 @@ export type InternalRouterRoute = Omit<RouterRoute, 'handler'> & {
/** @internal */
export interface RouterOptions {
/** Whether we are running in development */
isDev?: boolean;
env: Env;
/** Plugin for which this router was registered */
pluginId?: symbol;
@ -203,7 +203,7 @@ export class Router<Context extends RequestHandlerContextBase = RequestHandlerCo
if (getProtocolFromRequest(request) === 'http2' && kibanaResponse.options.headers) {
kibanaResponse.options.headers = stripIllegalHttp2Headers({
headers: kibanaResponse.options.headers,
isDev: this.options.isDev ?? false,
isDev: this.options.env.mode.dev,
logger: this.log,
requestContext: `${request.route.method} ${request.route.path}`,
});
@ -233,7 +233,7 @@ export class Router<Context extends RequestHandlerContextBase = RequestHandlerCo
if (this.versionedRouter === undefined) {
this.versionedRouter = CoreVersionedRouter.from({
router: this,
isDev: this.options.isDev,
env: this.options.env,
log: this.log,
...this.options.versionedRouterOptions,
});

View file

@ -21,6 +21,12 @@ 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';
import { getEnvOptions, createTestEnv } from '@kbn/config-mocks';
const notDevOptions = getEnvOptions();
notDevOptions.cliArgs.dev = false;
const notDevEnv = createTestEnv({ envOptions: notDevOptions });
const devEnv = createTestEnv();
describe('Versioned route', () => {
let router: Router;
@ -33,6 +39,7 @@ describe('Versioned route', () => {
versionedRouter = CoreVersionedRouter.from({
router,
log: loggingSystemMock.createLogger(),
env: notDevEnv,
});
});
@ -198,7 +205,7 @@ describe('Versioned route', () => {
it('allows public versions other than "2023-10-31"', () => {
expect(() =>
CoreVersionedRouter.from({ router, log: loggingSystemMock.createLogger(), isDev: false })
CoreVersionedRouter.from({ router, log: loggingSystemMock.createLogger(), env: notDevEnv })
.get({ access: 'public', path: '/foo' })
.addVersion({ version: '2023-01-31', validate: false }, (ctx, req, res) => res.ok())
).not.toThrow();
@ -297,7 +304,7 @@ describe('Versioned route', () => {
beforeEach(() => {
versionedRouter = CoreVersionedRouter.from({
router,
isDev: true,
env: devEnv,
log: loggingSystemMock.createLogger(),
});
});
@ -346,7 +353,7 @@ describe('Versioned route', () => {
(router.registerRoute as jest.Mock).mockImplementation((opts) => (handler = opts.handler));
versionedRouter = CoreVersionedRouter.from({
router,
isDev: true,
env: devEnv,
log: loggingSystemMock.createLogger(),
});
versionedRouter.post({ path: '/test/{id}', access: 'internal' }).addVersion(
@ -379,7 +386,7 @@ describe('Versioned route', () => {
(router.registerRoute as jest.Mock).mockImplementation((opts) => (handler = opts.handler));
versionedRouter = CoreVersionedRouter.from({
router,
isDev: true,
env: devEnv,
log: loggingSystemMock.createLogger(),
});
versionedRouter.post({ path: '/test/{id}', access: 'internal' }).addVersion(
@ -411,7 +418,7 @@ describe('Versioned route', () => {
it('allows using default resolution for specific internal routes', async () => {
versionedRouter = CoreVersionedRouter.from({
router,
isDev: true,
env: devEnv,
log: loggingSystemMock.createLogger(),
useVersionResolutionStrategyForInternalPaths: ['/bypass_me/{id?}'],
});

View file

@ -25,6 +25,7 @@ import type {
} from '@kbn/core-http-server';
import { Request } from '@hapi/hapi';
import { Logger } from '@kbn/logging';
import { Env } from '@kbn/config';
import type { HandlerResolutionStrategy, Method, Options } from './types';
import {
@ -34,7 +35,7 @@ import {
readVersion,
removeQueryVersion,
} from './route_version_utils';
import { injectVersionHeader } from '../util';
import { injectResponseHeaders, injectVersionHeader } from '../util';
import { validRouteSecurity } from '../security_route_config_validator';
import { resolvers } from './handler_resolvers';
@ -44,9 +45,10 @@ import { RequestHandlerEnhanced, Router } from '../router';
import { kibanaResponseFactory as responseFactory } from '../response';
import { validateHapiRequest } from '../route';
import { RouteValidator } from '../validator';
import { getWarningHeaderMessageFromRouteDeprecation } from '../get_warning_header_message';
interface InternalVersionedRouteConfig<M extends RouteMethod> extends VersionedRouteConfig<M> {
isDev: boolean;
env: Env;
useVersionResolutionStrategyForInternalPaths: Map<string, boolean>;
defaultHandlerResolutionStrategy: HandlerResolutionStrategy;
}
@ -86,7 +88,7 @@ export class CoreVersionedRoute implements VersionedRoute {
private useDefaultStrategyForPath: boolean;
private isPublic: boolean;
private isDev: boolean;
private env: Env;
private enableQueryVersion: boolean;
private defaultSecurityConfig: RouteSecurity | undefined;
private defaultHandlerResolutionStrategy: HandlerResolutionStrategy;
@ -98,13 +100,13 @@ export class CoreVersionedRoute implements VersionedRoute {
internalOptions: InternalVersionedRouteConfig<Method>
) {
const {
isDev,
env,
useVersionResolutionStrategyForInternalPaths,
defaultHandlerResolutionStrategy,
...options
} = internalOptions;
this.isPublic = options.access === 'public';
this.isDev = isDev;
this.env = env;
this.defaultHandlerResolutionStrategy = defaultHandlerResolutionStrategy;
this.useDefaultStrategyForPath =
this.isPublic || useVersionResolutionStrategyForInternalPaths.has(path);
@ -146,7 +148,7 @@ export class CoreVersionedRoute implements VersionedRoute {
if (!maybeVersion) {
if (this.useDefaultStrategyForPath) {
version = this.getDefaultVersion();
} else if (!this.isDev && !this.isPublic) {
} else if (!this.env.mode.dev && !this.isPublic) {
// When in production, we default internal routes to v1 to allow
// gracefully onboarding of un-versioned to versioned routes
version = '1';
@ -211,9 +213,22 @@ export class CoreVersionedRoute implements VersionedRoute {
return injectVersionHeader(version, error);
}
const response = await handler.fn(kibanaRequest, responseFactory);
let response = await handler.fn(kibanaRequest, responseFactory);
if (this.isDev && validation?.response?.[response.status]?.body) {
// we don't want to overwrite the header value
if (handler.options.options?.deprecated && !response.options.headers?.warning) {
response = injectResponseHeaders(
{
warning: getWarningHeaderMessageFromRouteDeprecation(
handler.options.options.deprecated,
this.env.packageInfo.version
),
},
response
);
}
if (this.env.mode.dev && validation?.response?.[response.status]?.body) {
const { [response.status]: responseValidation, unsafe } = validation.response;
try {
const validator = RouteValidator.from({
@ -235,7 +250,7 @@ export class CoreVersionedRoute implements VersionedRoute {
private validateVersion(version: string) {
// We do an additional check here while we only have a single allowed public version
// for all public Kibana HTTP APIs
if (this.isDev && this.isPublic) {
if (this.env.mode.dev && this.isPublic) {
const message = isAllowedPublicVersion(version);
if (message) {
throw new Error(message);

View file

@ -11,6 +11,7 @@ import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
import { Router } from '../router';
import { CoreVersionedRouter } from '.';
import { createRouter } from './mocks';
import { createTestEnv } from '@kbn/config-mocks';
const pluginId = Symbol('test');
describe('Versioned router', () => {
@ -21,6 +22,7 @@ describe('Versioned router', () => {
versionedRouter = CoreVersionedRouter.from({
router,
log: loggingSystemMock.createLogger(),
env: createTestEnv(),
});
});

View file

@ -15,6 +15,7 @@ import type {
} from '@kbn/core-http-server';
import { omit } from 'lodash';
import { Logger } from '@kbn/logging';
import { Env } from '@kbn/config';
import { CoreVersionedRoute } from './core_versioned_route';
import type { HandlerResolutionStrategy, Method } from './types';
import type { Router } from '../router';
@ -30,7 +31,7 @@ export interface VersionedRouterArgs {
*/
defaultHandlerResolutionStrategy?: HandlerResolutionStrategy;
/** Whether Kibana is running in a dev environment */
isDev?: boolean;
env: Env;
/**
* List of internal paths that should use the default handler resolution strategy. By default this
* is no routes ([]) because ONLY Elastic clients are intended to call internal routes.
@ -57,14 +58,14 @@ export class CoreVersionedRouter implements VersionedRouter {
router,
log,
defaultHandlerResolutionStrategy,
isDev,
env,
useVersionResolutionStrategyForInternalPaths,
}: VersionedRouterArgs) {
return new CoreVersionedRouter(
router,
log,
defaultHandlerResolutionStrategy,
isDev,
env,
useVersionResolutionStrategyForInternalPaths
);
}
@ -72,7 +73,7 @@ export class CoreVersionedRouter implements VersionedRouter {
public readonly router: Router,
private readonly log: Logger,
public readonly defaultHandlerResolutionStrategy: HandlerResolutionStrategy = 'oldest',
public readonly isDev: boolean = false,
public readonly env: Env,
useVersionResolutionStrategyForInternalPaths: string[] = []
) {
this.pluginId = this.router.pluginId;
@ -94,7 +95,7 @@ export class CoreVersionedRouter implements VersionedRouter {
defaultHandlerResolutionStrategy: this.defaultHandlerResolutionStrategy,
useVersionResolutionStrategyForInternalPaths:
this.useVersionResolutionStrategyForInternalPaths,
isDev: this.isDev,
env: this.env,
},
});
this.routes.add(route);

View file

@ -19,7 +19,9 @@
"@kbn/core-logging-server-mocks",
"@kbn/logging",
"@kbn/core-http-common",
"@kbn/logging-mocks"
"@kbn/logging-mocks",
"@kbn/config-mocks",
"@kbn/config"
],
"exclude": [
"target/**/*",

View file

@ -32,9 +32,14 @@ import { KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils';
import moment from 'moment';
import { of, Observable, BehaviorSubject } from 'rxjs';
import { mockCoreContext } from '@kbn/core-base-server-mocks';
import { createTestEnv, getEnvOptions } from '@kbn/config-mocks';
const options = getEnvOptions();
options.cliArgs.dev = false;
const env = createTestEnv({ envOptions: options });
const routerOptions: RouterOptions = {
isDev: false,
env,
versionedRouterOptions: {
defaultHandlerResolutionStrategy: 'oldest',
useVersionResolutionStrategyForInternalPaths: [],

View file

@ -486,7 +486,7 @@ test('passes versioned config to router', async () => {
expect.any(Object), // logger
expect.any(Function), // context enhancer
expect.objectContaining({
isDev: true,
env,
versionedRouterOptions: {
defaultHandlerResolutionStrategy: 'newest',
useVersionResolutionStrategyForInternalPaths: ['/foo'],

View file

@ -147,7 +147,7 @@ export class HttpService
this.log,
prebootServerRequestHandlerContext.createHandler.bind(null, this.coreContext.coreId),
{
isDev: this.env.mode.dev,
env: this.env,
versionedRouterOptions: getVersionedRouterOptions(config),
}
);
@ -196,7 +196,7 @@ export class HttpService
) => {
const enhanceHandler = this.requestHandlerContext!.createHandler.bind(null, pluginId);
const router = new Router<Context>(path, this.log, enhanceHandler, {
isDev: this.env.mode.dev,
env: this.env,
versionedRouterOptions: getVersionedRouterOptions(config),
pluginId,
});

View file

@ -66,11 +66,13 @@ export function adoptToHapiOnPreResponseFormat(fn: OnPreResponseHandler, log: Lo
try {
if (response) {
const statusCode: number = isBoom(response)
const isResponseBoom = isBoom(response);
const statusCode: number = isResponseBoom
? response.output.statusCode
: response.statusCode;
const headers: ResponseHeaders = isResponseBoom ? {} : response.headers;
const result = await fn(CoreKibanaRequest.from(request), { statusCode }, toolkit);
const result = await fn(CoreKibanaRequest.from(request), { statusCode, headers }, toolkit);
if (preResponseResult.isNext(result)) {
if (result.headers) {

View file

@ -22,6 +22,7 @@ import {
INTERNAL_API_RESTRICTED_LOGGER_NAME,
createBuildNrMismatchLoggerPreResponseHandler,
createCustomHeadersPreResponseHandler,
createDeprecationWarningHeaderPreResponseHandler,
createRestrictInternalRoutesPostAuthHandler,
createVersionCheckPostAuthHandler,
createXsrfPostAuthHandler,
@ -547,6 +548,50 @@ describe('customHeaders pre-response handler', () => {
});
});
describe('deprecation header pre-response handler', () => {
let toolkit: ToolkitMock;
beforeEach(() => {
toolkit = createToolkit();
});
it('adds the deprecation warning header to the request going to a deprecated route', () => {
const kibanaVersion = '19.73.41';
const deprecationMessage = 'This is a deprecated endpoint message in the tests';
const warningHeader = `299 Kibana-${kibanaVersion} "${deprecationMessage}"`;
const handler = createDeprecationWarningHeaderPreResponseHandler(kibanaVersion);
handler(
{ route: { options: { deprecated: { message: deprecationMessage } } } } as any,
{} as any,
toolkit
);
expect(toolkit.next).toHaveBeenCalledTimes(1);
expect(toolkit.next).toHaveBeenCalledWith({
headers: {
warning: warningHeader,
},
});
});
it('does not add the deprecation warning header to the request going to a non-deprecated route', () => {
const kibanaVersion = '19.73.41';
const deprecationMessage = 'This is a deprecated endpoint message in the tests';
const warningHeader = `299 Kibana-${kibanaVersion} "${deprecationMessage}"`;
const handler = createDeprecationWarningHeaderPreResponseHandler(kibanaVersion);
handler({ route: { options: { deprecated: {} } } } as any, {} as any, toolkit);
expect(toolkit.next).toHaveBeenCalledTimes(1);
expect(toolkit.next).not.toHaveBeenCalledWith({
headers: {
warning: warningHeader,
},
});
});
});
describe('build number mismatch logger on error pre-response handler', () => {
let logger: jest.Mocked<Logger>;

View file

@ -13,7 +13,10 @@ import type {
OnPreResponseInfo,
KibanaRequest,
} from '@kbn/core-http-server';
import { isSafeMethod } from '@kbn/core-http-router-server-internal';
import {
getWarningHeaderMessageFromRouteDeprecation,
isSafeMethod,
} from '@kbn/core-http-router-server-internal';
import { Logger } from '@kbn/logging';
import { KIBANA_BUILD_NR_HEADER } from '@kbn/core-http-common';
import { HttpConfig } from './http_config';
@ -120,6 +123,24 @@ export const createCustomHeadersPreResponseHandler = (config: HttpConfig): OnPre
};
};
export const createDeprecationWarningHeaderPreResponseHandler = (
kibanaVersion: string
): OnPreResponseHandler => {
return (request, response, toolkit) => {
// we don't want to overwrite the header value
if (!request.route.options.deprecated || response.headers?.warning) {
return toolkit.next();
}
const additionalHeaders = {
warning: getWarningHeaderMessageFromRouteDeprecation(
request.route.options.deprecated,
kibanaVersion
),
};
return toolkit.next({ headers: { ...additionalHeaders } });
};
};
const shouldLogBuildNumberMismatch = (
serverBuild: { number: number; string: string },
request: KibanaRequest,

View file

@ -17,6 +17,7 @@ import {
createVersionCheckPostAuthHandler,
createBuildNrMismatchLoggerPreResponseHandler,
createXsrfPostAuthHandler,
createDeprecationWarningHeaderPreResponseHandler,
} from './lifecycle_handlers';
export const registerCoreHandlers = (
@ -27,6 +28,10 @@ export const registerCoreHandlers = (
) => {
// add headers based on config
registrar.registerOnPreResponse(createCustomHeadersPreResponseHandler(config));
// add headers for deprecated endpoints
registrar.registerOnPreResponse(
createDeprecationWarningHeaderPreResponseHandler(env.packageInfo.version)
);
// add extra request checks stuff
registrar.registerOnPostAuth(createXsrfPostAuthHandler(config));
if (config.versioned.strictClientVersionCheck !== false) {

View file

@ -65,6 +65,8 @@ export interface OnPreResponseExtensions {
*/
export interface OnPreResponseInfo {
statusCode: number;
/** So any pre response handler can check the headers if needed, to avoid an overwrite for example */
headers?: ResponseHeaders;
}
/**

View file

@ -128,6 +128,8 @@ export interface RouteDeprecationInfo {
documentationUrl: string;
/**
* The description message to be displayed for the deprecation.
* This will also appear in the '299 Kibana-{version} {message}' header warning when someone calls the route.
* Keep the message concise to avoid long header values. It is recommended to keep the message under 255 characters.
* Check the README for writing deprecations in `src/core/server/deprecations/README.mdx`
*/
message?: string;

View file

@ -22,6 +22,11 @@ import {
} from '@kbn/core-http-server-internal';
import { mockCoreContext } from '@kbn/core-base-server-mocks';
import type { Logger } from '@kbn/logging';
import { createTestEnv, getEnvOptions } from '@kbn/config-mocks';
const options = getEnvOptions();
options.cliArgs.dev = false;
const env = createTestEnv({ envOptions: options });
const CSP_CONFIG = cspConfig.schema.validate({});
const EXTERNAL_URL_CONFIG = externalUrlConfig.schema.validate({});
@ -74,7 +79,7 @@ describe('Http2 - Smoke tests', () => {
innerServerListener = innerServer.listener;
const router = new Router('', logger, enhanceWithContext, {
isDev: false,
env,
versionedRouterOptions: {
defaultHandlerResolutionStrategy: 'oldest',
},
@ -177,7 +182,7 @@ describe('Http2 - Smoke tests', () => {
innerServerListener = innerServer.listener;
const router = new Router('', logger, enhanceWithContext, {
isDev: false,
env,
versionedRouterOptions: {
defaultHandlerResolutionStrategy: 'oldest',
},

View file

@ -16,6 +16,11 @@ import { Router } from '@kbn/core-http-router-server-internal';
import { HttpServer, HttpConfig } from '@kbn/core-http-server-internal';
import { mockCoreContext } from '@kbn/core-base-server-mocks';
import type { Logger } from '@kbn/logging';
import { createTestEnv, getEnvOptions } from '@kbn/config-mocks';
const options = getEnvOptions();
options.cliArgs.dev = false;
const env = createTestEnv({ envOptions: options });
describe('Http server', () => {
let server: HttpServer;
@ -60,7 +65,7 @@ describe('Http server', () => {
innerServerListener = innerServer.listener;
const router = new Router('', logger, enhanceWithContext, {
isDev: false,
env,
versionedRouterOptions: {
defaultHandlerResolutionStrategy: 'oldest',
},

View file

@ -16,6 +16,9 @@ import { contextServiceMock } from '@kbn/core-http-context-server-mocks';
import { ensureRawRequest } from '@kbn/core-http-router-server-internal';
import { HttpService } from '@kbn/core-http-server-internal';
import { createHttpService } from '@kbn/core-http-server-mocks';
import { Env } from '@kbn/config';
import { REPO_ROOT } from '@kbn/repo-info';
import { getEnvOptions } from '@kbn/config-mocks';
let server: HttpService;
@ -28,6 +31,8 @@ const setupDeps = {
executionContext: executionContextServiceMock.createInternalSetupContract(),
};
const kibanaVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version;
beforeEach(async () => {
logger = loggingSystemMock.create();
server = createHttpService({ logger });
@ -1503,6 +1508,195 @@ describe('runs with default preResponse handlers', () => {
});
});
describe('runs with default preResponse deprecation handlers', () => {
const deprecationMessage = 'This is a deprecated endpoint for testing reasons';
const warningString = `299 Kibana-${kibanaVersion} "${deprecationMessage}"`;
it('should handle a deprecated route and include deprecation warning headers', async () => {
const { server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
router.get(
{
path: '/deprecated',
validate: false,
options: {
deprecated: {
documentationUrl: 'https://fake-url.com',
reason: { type: 'deprecate' },
severity: 'warning',
message: deprecationMessage,
},
},
},
(context, req, res) => res.ok({})
);
await server.start();
const response = await supertest(innerServer.listener).get('/deprecated').expect(200);
expect(response.header.warning).toMatch(warningString);
});
it('should not add a deprecation warning header to a non deprecated route', async () => {
const { server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
router.get(
{
path: '/test',
validate: false,
},
(context, req, res) => res.ok({})
);
await server.start();
const response = await supertest(innerServer.listener).get('/test').expect(200);
expect(response.header.warning).toBeUndefined();
});
it('should not overwrite the warning header if it was already set', async () => {
const { server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
const expectedWarningHeader = 'This should not get overwritten';
router.get(
{
path: '/deprecated',
validate: false,
options: {
deprecated: {
documentationUrl: 'https://fake-url.com',
reason: { type: 'deprecate' },
severity: 'warning',
message: deprecationMessage,
},
},
},
(context, req, res) => res.ok({ headers: { warning: expectedWarningHeader } })
);
await server.start();
const response = await supertest(innerServer.listener).get('/deprecated').expect(200);
expect(response.header.warning).toMatch(expectedWarningHeader);
});
it('should return the warning header in deprecated v1 but not in non deprecated v2', async () => {
const { server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
router.versioned
.get({
access: 'internal',
path: '/test',
})
.addVersion(
{
version: '1',
validate: false,
options: {
deprecated: {
documentationUrl: 'https://fake-url.com',
reason: { type: 'deprecate' },
severity: 'warning',
message: deprecationMessage,
},
},
},
async (ctx, req, res) => {
return res.ok({ body: { v: '1' } });
}
)
.addVersion(
{
version: '2',
validate: false,
},
async (ctx, req, res) => {
return res.ok({ body: { v: '2' } });
}
);
await server.start();
let response = await supertest(innerServer.listener)
.get('/test')
.set('Elastic-Api-Version', '1')
.expect(200);
expect(response.body.v).toMatch('1');
expect(response.header.warning).toMatch(warningString);
response = await supertest(innerServer.listener)
.get('/test')
.set('Elastic-Api-Version', '2')
.expect(200);
expect(response.body.v).toMatch('2');
expect(response.header.warning).toBeUndefined();
});
it('should not overwrite the warning header if it was already set (versioned)', async () => {
const { server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
const expectedWarningHeader = 'This should not get overwritten';
router.versioned
.get({
access: 'internal',
path: '/test',
})
.addVersion(
{
version: '1',
validate: false,
options: {
deprecated: {
documentationUrl: 'https://fake-url.com',
reason: { type: 'deprecate' },
severity: 'warning',
message: deprecationMessage,
},
},
},
async (ctx, req, res) => {
return res.ok({ body: { v: '1' }, headers: { warning: expectedWarningHeader } });
}
)
.addVersion(
{
version: '2',
validate: false,
},
async (ctx, req, res) => {
return res.ok({ body: { v: '2' } });
}
);
await server.start();
let response = await supertest(innerServer.listener)
.get('/test')
.set('Elastic-Api-Version', '1')
.expect(200);
expect(response.body.v).toMatch('1');
expect(response.header.warning).toMatch(expectedWarningHeader);
response = await supertest(innerServer.listener)
.get('/test')
.set('Elastic-Api-Version', '2')
.expect(200);
expect(response.body.v).toMatch('2');
expect(response.header.warning).toBeUndefined();
});
});
describe('run interceptors in the right order', () => {
it('with Auth registered', async () => {
const {

View file

@ -22,6 +22,11 @@ import { Router } from '@kbn/core-http-router-server-internal';
import { createHttpService } from '@kbn/core-http-server-mocks';
import type { HttpService } from '@kbn/core-http-server-internal';
import { loggerMock } from '@kbn/logging-mocks';
import { createTestEnv, getEnvOptions } from '@kbn/config-mocks';
const options = getEnvOptions();
options.cliArgs.dev = false;
const env = createTestEnv({ envOptions: options });
let server: HttpService;
let logger: ReturnType<typeof loggingSystemMock.create>;
@ -2266,7 +2271,7 @@ describe('registerRouterAfterListening', () => {
const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {});
const otherRouter = new Router('/test', loggerMock.create(), enhanceWithContext, {
isDev: false,
env,
versionedRouterOptions: {
defaultHandlerResolutionStrategy: 'oldest',
},
@ -2303,7 +2308,7 @@ describe('registerRouterAfterListening', () => {
const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {});
const otherRouter = new Router('/test', loggerMock.create(), enhanceWithContext, {
isDev: false,
env,
versionedRouterOptions: {
defaultHandlerResolutionStrategy: 'oldest',
},

View file

@ -23,6 +23,11 @@ import {
import { isServerTLS, flattenCertificateChain, fetchPeerCertificate } from './tls_utils';
import { mockCoreContext } from '@kbn/core-base-server-mocks';
import type { Logger } from '@kbn/logging';
import { createTestEnv, getEnvOptions } from '@kbn/config-mocks';
const options = getEnvOptions();
options.cliArgs.dev = false;
const env = createTestEnv({ envOptions: options });
const CSP_CONFIG = cspConfig.schema.validate({});
const EXTERNAL_URL_CONFIG = externalUrlConfig.schema.validate({});
@ -70,7 +75,7 @@ describe('HttpServer - TLS config', () => {
const listener = innerServer.listener;
const router = new Router('', logger, enhanceWithContext, {
isDev: false,
env,
versionedRouterOptions: {
defaultHandlerResolutionStrategy: 'oldest',
},