Skip to content

Commit

Permalink
[8.x] [Http] Router refactor (#205502) (#207828)
Browse files Browse the repository at this point in the history
# Backport

This will backport the following commits from `main` to `8.x`:
- [[Http] Router refactor
(#205502)](#205502)

<!--- Backport version: 9.6.4 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Jean-Louis
Leysens","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-01-15T15:10:46Z","message":"[Http]
Router refactor
(#205502)","sha":"ca77772d2ad5db6bb1c7b9412845e59d7a59d3ac","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Feature:http","Team:Core","release_note:skip","backport
missing","v9.0.0","backport:version","v8.18.0"],"title":"[Http] Router
refactor","number":205502,"url":"https://github.com/elastic/kibana/pull/205502","mergeCommit":{"message":"[Http]
Router refactor
(#205502)","sha":"ca77772d2ad5db6bb1c7b9412845e59d7a59d3ac"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/205502","number":205502,"mergeCommit":{"message":"[Http]
Router refactor
(#205502)","sha":"ca77772d2ad5db6bb1c7b9412845e59d7a59d3ac"}},{"branch":"8.x","label":"v8.18.0","branchLabelMappingKey":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->
  • Loading branch information
jloleysens authored Jan 22, 2025
1 parent a14c51c commit 12fe799
Show file tree
Hide file tree
Showing 22 changed files with 823 additions and 486 deletions.
2 changes: 1 addition & 1 deletion src/core/packages/http/router-server-internal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
10 changes: 9 additions & 1 deletion src/core/packages/http/router-server-internal/src/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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));
}
188 changes: 188 additions & 0 deletions src/core/packages/http/router-server-internal/src/route.test.ts
Original file line number Diff line number Diff line change
@@ -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,
});
}
});
});
Loading

0 comments on commit 12fe799

Please sign in to comment.