Skip to content

Commit 5363f01

Browse files
committed
[NP] KibanaRequest provides request abortion event (#55061)
* add aborted$ observable to KibanaRequest * complete observable on request end * update docs * update test suit names * always finish subscription * address comments
1 parent 88b15ff commit 5363f01

12 files changed

+216
-4
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
2+
3+
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [KibanaRequest](./kibana-plugin-server.kibanarequest.md) &gt; [events](./kibana-plugin-server.kibanarequest.events.md)
4+
5+
## KibanaRequest.events property
6+
7+
Request events [KibanaRequestEvents](./kibana-plugin-server.kibanarequestevents.md)
8+
9+
<b>Signature:</b>
10+
11+
```typescript
12+
readonly events: KibanaRequestEvents;
13+
```

docs/development/core/server/kibana-plugin-server.kibanarequest.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ export declare class KibanaRequest<Params = unknown, Query = unknown, Body = unk
2323
| Property | Modifiers | Type | Description |
2424
| --- | --- | --- | --- |
2525
| [body](./kibana-plugin-server.kibanarequest.body.md) | | <code>Body</code> | |
26+
| [events](./kibana-plugin-server.kibanarequest.events.md) | | <code>KibanaRequestEvents</code> | Request events [KibanaRequestEvents](./kibana-plugin-server.kibanarequestevents.md) |
2627
| [headers](./kibana-plugin-server.kibanarequest.headers.md) | | <code>Headers</code> | Readonly copy of incoming request headers. |
2728
| [params](./kibana-plugin-server.kibanarequest.params.md) | | <code>Params</code> | |
2829
| [query](./kibana-plugin-server.kibanarequest.query.md) | | <code>Query</code> | |
2930
| [route](./kibana-plugin-server.kibanarequest.route.md) | | <code>RecursiveReadonly&lt;KibanaRequestRoute&lt;Method&gt;&gt;</code> | matched route details |
30-
| [socket](./kibana-plugin-server.kibanarequest.socket.md) | | <code>IKibanaSocket</code> | |
31+
| [socket](./kibana-plugin-server.kibanarequest.socket.md) | | <code>IKibanaSocket</code> | [IKibanaSocket](./kibana-plugin-server.ikibanasocket.md) |
3132
| [url](./kibana-plugin-server.kibanarequest.url.md) | | <code>Url</code> | a WHATWG URL standard object. |
3233

docs/development/core/server/kibana-plugin-server.kibanarequest.socket.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
## KibanaRequest.socket property
66

7+
[IKibanaSocket](./kibana-plugin-server.ikibanasocket.md)
8+
79
<b>Signature:</b>
810

911
```typescript
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
2+
3+
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [KibanaRequestEvents](./kibana-plugin-server.kibanarequestevents.md) &gt; [aborted$](./kibana-plugin-server.kibanarequestevents.aborted_.md)
4+
5+
## KibanaRequestEvents.aborted$ property
6+
7+
Observable that emits once if and when the request has been aborted.
8+
9+
<b>Signature:</b>
10+
11+
```typescript
12+
aborted$: Observable<void>;
13+
```
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
2+
3+
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [KibanaRequestEvents](./kibana-plugin-server.kibanarequestevents.md)
4+
5+
## KibanaRequestEvents interface
6+
7+
Request events.
8+
9+
<b>Signature:</b>
10+
11+
```typescript
12+
export interface KibanaRequestEvents
13+
```
14+
15+
## Properties
16+
17+
| Property | Type | Description |
18+
| --- | --- | --- |
19+
| [aborted$](./kibana-plugin-server.kibanarequestevents.aborted_.md) | <code>Observable&lt;void&gt;</code> | Observable that emits once if and when the request has been aborted. |
20+

docs/development/core/server/kibana-plugin-server.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
7676
| [IRouter](./kibana-plugin-server.irouter.md) | Registers route handlers for specified resource path and method. See [RouteConfig](./kibana-plugin-server.routeconfig.md) and [RequestHandler](./kibana-plugin-server.requesthandler.md) for more information about arguments to route registrations. |
7777
| [IScopedRenderingClient](./kibana-plugin-server.iscopedrenderingclient.md) | |
7878
| [IUiSettingsClient](./kibana-plugin-server.iuisettingsclient.md) | Server-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. |
79+
| [KibanaRequestEvents](./kibana-plugin-server.kibanarequestevents.md) | Request events. |
7980
| [KibanaRequestRoute](./kibana-plugin-server.kibanarequestroute.md) | Request specific route information exposed to a handler. |
8081
| [LegacyRequest](./kibana-plugin-server.legacyrequest.md) | |
8182
| [LegacyServiceSetupDeps](./kibana-plugin-server.legacyservicesetupdeps.md) | |

src/core/server/http/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export {
2929
HttpResponsePayload,
3030
ErrorHttpResponseOptions,
3131
KibanaRequest,
32+
KibanaRequestEvents,
3233
KibanaRequestRoute,
3334
KibanaRequestRouteOptions,
3435
IKibanaResponse,
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
import supertest from 'supertest';
20+
21+
import { HttpService } from '../http_service';
22+
23+
import { contextServiceMock } from '../../context/context_service.mock';
24+
import { loggingServiceMock } from '../../logging/logging_service.mock';
25+
import { createHttpServer } from '../test_utils';
26+
27+
let server: HttpService;
28+
29+
let logger: ReturnType<typeof loggingServiceMock.create>;
30+
const contextSetup = contextServiceMock.createSetupContract();
31+
32+
const setupDeps = {
33+
context: contextSetup,
34+
};
35+
36+
beforeEach(() => {
37+
logger = loggingServiceMock.create();
38+
39+
server = createHttpServer({ logger });
40+
});
41+
42+
afterEach(async () => {
43+
await server.stop();
44+
});
45+
46+
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
47+
describe('KibanaRequest', () => {
48+
describe('events', () => {
49+
describe('aborted$', () => {
50+
it('emits once and completes when request aborted', async done => {
51+
expect.assertions(1);
52+
const { server: innerServer, createRouter } = await server.setup(setupDeps);
53+
const router = createRouter('/');
54+
55+
const nextSpy = jest.fn();
56+
router.get({ path: '/', validate: false }, async (context, request, res) => {
57+
request.events.aborted$.subscribe({
58+
next: nextSpy,
59+
complete: () => {
60+
expect(nextSpy).toHaveBeenCalledTimes(1);
61+
done();
62+
},
63+
});
64+
65+
// prevents the server to respond
66+
await delay(30000);
67+
return res.ok({ body: 'ok' });
68+
});
69+
70+
await server.start();
71+
72+
const incomingRequest = supertest(innerServer.listener)
73+
.get('/')
74+
// end required to send request
75+
.end();
76+
77+
setTimeout(() => incomingRequest.abort(), 50);
78+
});
79+
80+
it('completes & does not emit when request handled', async () => {
81+
const { server: innerServer, createRouter } = await server.setup(setupDeps);
82+
const router = createRouter('/');
83+
84+
const nextSpy = jest.fn();
85+
const completeSpy = jest.fn();
86+
router.get({ path: '/', validate: false }, async (context, request, res) => {
87+
request.events.aborted$.subscribe({
88+
next: nextSpy,
89+
complete: completeSpy,
90+
});
91+
92+
return res.ok({ body: 'ok' });
93+
});
94+
95+
await server.start();
96+
97+
await supertest(innerServer.listener).get('/');
98+
99+
expect(nextSpy).toHaveBeenCalledTimes(0);
100+
expect(completeSpy).toHaveBeenCalledTimes(1);
101+
});
102+
103+
it('completes & does not emit when request rejected', async () => {
104+
const { server: innerServer, createRouter } = await server.setup(setupDeps);
105+
const router = createRouter('/');
106+
107+
const nextSpy = jest.fn();
108+
const completeSpy = jest.fn();
109+
router.get({ path: '/', validate: false }, async (context, request, res) => {
110+
request.events.aborted$.subscribe({
111+
next: nextSpy,
112+
complete: completeSpy,
113+
});
114+
115+
return res.badRequest();
116+
});
117+
118+
await server.start();
119+
120+
await supertest(innerServer.listener).get('/');
121+
122+
expect(nextSpy).toHaveBeenCalledTimes(0);
123+
expect(completeSpy).toHaveBeenCalledTimes(1);
124+
});
125+
});
126+
});
127+
});

src/core/server/http/router/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export { Headers, filterHeaders, ResponseHeaders, KnownHeaders } from './headers
2121
export { Router, RequestHandler, IRouter, RouteRegistrar } from './router';
2222
export {
2323
KibanaRequest,
24+
KibanaRequestEvents,
2425
KibanaRequestRoute,
2526
KibanaRequestRouteOptions,
2627
isRealRequest,

src/core/server/http/router/request.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
import { Url } from 'url';
2121
import { Request } from 'hapi';
22+
import { Observable, fromEvent, merge } from 'rxjs';
23+
import { shareReplay, first, takeUntil } from 'rxjs/operators';
2224

2325
import { deepFreeze, RecursiveReadonly } from '../../../utils';
2426
import { Headers } from './headers';
@@ -46,6 +48,17 @@ export interface KibanaRequestRoute<Method extends RouteMethod> {
4648
options: KibanaRequestRouteOptions<Method>;
4749
}
4850

51+
/**
52+
* Request events.
53+
* @public
54+
* */
55+
export interface KibanaRequestEvents {
56+
/**
57+
* Observable that emits once if and when the request has been aborted.
58+
*/
59+
aborted$: Observable<void>;
60+
}
61+
4962
/**
5063
* @deprecated
5164
* `hapi` request object, supported during migration process only for backward compatibility.
@@ -115,7 +128,10 @@ export class KibanaRequest<
115128
*/
116129
public readonly headers: Headers;
117130

131+
/** {@link IKibanaSocket} */
118132
public readonly socket: IKibanaSocket;
133+
/** Request events {@link KibanaRequestEvents} */
134+
public readonly events: KibanaRequestEvents;
119135

120136
/** @internal */
121137
protected readonly [requestSymbol]: Request;
@@ -138,12 +154,22 @@ export class KibanaRequest<
138154
enumerable: false,
139155
});
140156

141-
this.route = deepFreeze(this.getRouteInfo());
157+
this.route = deepFreeze(this.getRouteInfo(request));
142158
this.socket = new KibanaSocket(request.raw.req.socket);
159+
this.events = this.getEvents(request);
160+
}
161+
162+
private getEvents(request: Request): KibanaRequestEvents {
163+
const finish$ = merge(
164+
fromEvent(request.raw.req, 'end'), // all data consumed
165+
fromEvent(request.raw.req, 'close') // connection was closed
166+
).pipe(shareReplay(1), first());
167+
return {
168+
aborted$: fromEvent<void>(request.raw.req, 'aborted').pipe(first(), takeUntil(finish$)),
169+
} as const;
143170
}
144171

145-
private getRouteInfo(): KibanaRequestRoute<Method> {
146-
const request = this[requestSymbol];
172+
private getRouteInfo(request: Request): KibanaRequestRoute<Method> {
147173
const method = request.method as Method;
148174
const { parse, maxBytes, allow, output } = request.route.settings.payload || {};
149175

0 commit comments

Comments
 (0)