Skip to content

Commit 12fe799

Browse files
authored
[8.x] [Http] Router refactor (#205502) (#207828)
# 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":"jeanlouis.leysens@elastic.co"},"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-->
1 parent a14c51c commit 12fe799

22 files changed

Lines changed: 823 additions & 486 deletions

‎src/core/packages/http/router-server-internal/index.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export {
1616
type HandlerResolutionStrategy,
1717
} from './src/versioned_router';
1818
export { Router } from './src/router';
19-
export type { RouterOptions, InternalRegistrar, InternalRegistrarOptions } from './src/router';
19+
export type { RouterOptions } from './src/router';
2020
export { isKibanaRequest, isRealRequest, ensureRawRequest, CoreKibanaRequest } from './src/request';
2121
export { isSafeMethod } from './src/route';
2222
export { HapiResponseAdapter } from './src/response_adapter';

‎src/core/packages/http/router-server-internal/src/request.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ export class CoreKibanaRequest<
191191
enumerable: false,
192192
});
193193

194-
this.httpVersion = isRealReq ? request.raw.req.httpVersion : '1.0';
194+
this.httpVersion = isRealReq ? getHttpVersionFromRequest(request) : '1.0';
195195
this.apiVersion = undefined;
196196
this.protocol = getProtocolFromHttpVersion(this.httpVersion);
197197

@@ -418,3 +418,11 @@ function sanitizeRequest(req: Request): { query: unknown; params: unknown; body:
418418
function getProtocolFromHttpVersion(httpVersion: string): HttpProtocol {
419419
return httpVersion.split('.')[0] === '2' ? 'http2' : 'http1';
420420
}
421+
422+
function getHttpVersionFromRequest(request: Request) {
423+
return request.raw.req.httpVersion;
424+
}
425+
426+
export function getProtocolFromRequest(request: Request) {
427+
return getProtocolFromHttpVersion(getHttpVersionFromRequest(request));
428+
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { hapiMocks } from '@kbn/hapi-mocks';
11+
import { validateHapiRequest, handle } from './route';
12+
import { createRouter } from './versioned_router/mocks';
13+
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
14+
import { Logger } from '@kbn/logging';
15+
import { RouteValidator } from './validator';
16+
import { schema } from '@kbn/config-schema';
17+
import { Router } from './router';
18+
import { RouteAccess } from '@kbn/core-http-server';
19+
import { createRequest } from './versioned_router/core_versioned_route.test.util';
20+
import { kibanaResponseFactory } from './response';
21+
22+
describe('handle', () => {
23+
let handler: jest.Func;
24+
let log: Logger;
25+
let router: Router;
26+
beforeEach(() => {
27+
router = createRouter();
28+
handler = jest.fn(async () => kibanaResponseFactory.ok());
29+
log = loggingSystemMock.createLogger();
30+
});
31+
describe('post validation events', () => {
32+
it('emits with validation schemas provided', async () => {
33+
const validate = { body: schema.object({ foo: schema.number() }) };
34+
await handle(createRequest({ body: { foo: 1 } }), {
35+
router,
36+
handler,
37+
log,
38+
method: 'get',
39+
route: { path: '/test', validate },
40+
routeSchemas: RouteValidator.from(validate),
41+
});
42+
// Failure
43+
await handle(createRequest({ body: { foo: 'bar' } }), {
44+
router,
45+
handler,
46+
log,
47+
method: 'get',
48+
route: {
49+
path: '/test',
50+
validate,
51+
options: {
52+
deprecated: {
53+
severity: 'warning',
54+
reason: { type: 'bump', newApiVersion: '123' },
55+
documentationUrl: 'http://test.foo',
56+
},
57+
},
58+
},
59+
routeSchemas: RouteValidator.from(validate),
60+
});
61+
62+
expect(handler).toHaveBeenCalledTimes(1);
63+
expect(router.emitPostValidate).toHaveBeenCalledTimes(2);
64+
65+
expect(router.emitPostValidate).toHaveBeenNthCalledWith(1, expect.any(Object), {
66+
deprecated: undefined,
67+
isInternalApiRequest: false,
68+
isPublicAccess: false,
69+
});
70+
expect(router.emitPostValidate).toHaveBeenNthCalledWith(2, expect.any(Object), {
71+
deprecated: {
72+
severity: 'warning',
73+
reason: { type: 'bump', newApiVersion: '123' },
74+
documentationUrl: 'http://test.foo',
75+
},
76+
isInternalApiRequest: false,
77+
isPublicAccess: false,
78+
});
79+
});
80+
81+
it('emits with no validation schemas provided', async () => {
82+
await handle(createRequest({ body: { foo: 1 } }), {
83+
router,
84+
handler,
85+
log,
86+
method: 'get',
87+
route: {
88+
path: '/test',
89+
validate: false,
90+
options: {
91+
deprecated: {
92+
severity: 'warning',
93+
reason: { type: 'bump', newApiVersion: '123' },
94+
documentationUrl: 'http://test.foo',
95+
},
96+
},
97+
},
98+
routeSchemas: undefined,
99+
});
100+
101+
expect(handler).toHaveBeenCalledTimes(1);
102+
expect(router.emitPostValidate).toHaveBeenCalledTimes(1);
103+
104+
expect(router.emitPostValidate).toHaveBeenCalledWith(expect.any(Object), {
105+
deprecated: {
106+
severity: 'warning',
107+
reason: { type: 'bump', newApiVersion: '123' },
108+
documentationUrl: 'http://test.foo',
109+
},
110+
isInternalApiRequest: false,
111+
isPublicAccess: false,
112+
});
113+
});
114+
});
115+
});
116+
117+
describe('validateHapiRequest', () => {
118+
let router: Router;
119+
let log: Logger;
120+
beforeEach(() => {
121+
router = createRouter();
122+
log = loggingSystemMock.createLogger();
123+
});
124+
it('validates hapi requests and returns kibana requests: ok case', () => {
125+
const { ok, error } = validateHapiRequest(hapiMocks.createRequest({ payload: { ok: true } }), {
126+
log,
127+
routeInfo: { access: 'public', httpResource: false },
128+
router,
129+
routeSchemas: RouteValidator.from({ body: schema.object({ ok: schema.literal(true) }) }),
130+
});
131+
expect(ok?.body).toEqual({ ok: true });
132+
expect(error).toBeUndefined();
133+
expect(log.error).not.toHaveBeenCalled();
134+
});
135+
it('validates hapi requests and returns kibana requests: error case', () => {
136+
const { ok, error } = validateHapiRequest(hapiMocks.createRequest({ payload: { ok: false } }), {
137+
log,
138+
routeInfo: { access: 'public', httpResource: false },
139+
router,
140+
routeSchemas: RouteValidator.from({ body: schema.object({ ok: schema.literal(true) }) }),
141+
});
142+
expect(ok).toBeUndefined();
143+
expect(error?.status).toEqual(400);
144+
expect(error?.payload).toMatch(/expected value to equal/);
145+
expect(log.error).toHaveBeenCalledTimes(1);
146+
expect(log.error).toHaveBeenCalledWith('400 Bad Request', {
147+
error: { message: '[request body.ok]: expected value to equal [true]' },
148+
http: { request: { method: undefined, path: undefined }, response: { status_code: 400 } },
149+
});
150+
});
151+
152+
it('emits post validation events on the router', () => {
153+
const deps = {
154+
log,
155+
routeInfo: { access: 'public' as RouteAccess, httpResource: false },
156+
router,
157+
routeSchemas: RouteValidator.from({ body: schema.object({ ok: schema.literal(true) }) }),
158+
};
159+
{
160+
const { ok, error } = validateHapiRequest(
161+
hapiMocks.createRequest({ payload: { ok: false } }),
162+
deps
163+
);
164+
expect(ok).toBeUndefined();
165+
expect(error).toBeDefined();
166+
expect(router.emitPostValidate).toHaveBeenCalledTimes(1);
167+
expect(router.emitPostValidate).toHaveBeenCalledWith(expect.any(Object), {
168+
deprecated: undefined,
169+
isInternalApiRequest: false,
170+
isPublicAccess: true,
171+
});
172+
}
173+
{
174+
const { ok, error } = validateHapiRequest(
175+
hapiMocks.createRequest({ payload: { ok: true } }),
176+
deps
177+
);
178+
expect(ok).toBeDefined();
179+
expect(error).toBeUndefined();
180+
expect(router.emitPostValidate).toHaveBeenCalledTimes(2);
181+
expect(router.emitPostValidate).toHaveBeenNthCalledWith(2, expect.any(Object), {
182+
deprecated: undefined,
183+
isInternalApiRequest: false,
184+
isPublicAccess: true,
185+
});
186+
}
187+
});
188+
});

0 commit comments

Comments
 (0)