Skip to content

Commit 4dba016

Browse files
authored
[7.9] Fix aborted$ event and add completed$ event to KibanaRequest (#73898) (#73964)
1 parent 074950d commit 4dba016

File tree

5 files changed

+128
-2
lines changed

5 files changed

+128
-2
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
2+
3+
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [KibanaRequestEvents](./kibana-plugin-core-server.kibanarequestevents.md) &gt; [completed$](./kibana-plugin-core-server.kibanarequestevents.completed_.md)
4+
5+
## KibanaRequestEvents.completed$ property
6+
7+
Observable that emits once if and when the request has been completely handled.
8+
9+
<b>Signature:</b>
10+
11+
```typescript
12+
completed$: Observable<void>;
13+
```
14+
15+
## Remarks
16+
17+
The request may be considered completed if: - A response has been sent to the client; or - The request was aborted.
18+

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ export interface KibanaRequestEvents
1717
| Property | Type | Description |
1818
| --- | --- | --- |
1919
| [aborted$](./kibana-plugin-core-server.kibanarequestevents.aborted_.md) | <code>Observable&lt;void&gt;</code> | Observable that emits once if and when the request has been aborted. |
20+
| [completed$](./kibana-plugin-core-server.kibanarequestevents.completed_.md) | <code>Observable&lt;void&gt;</code> | Observable that emits once if and when the request has been completely handled. |
2021

src/core/server/http/integration_tests/request.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { HttpService } from '../http_service';
2323
import { contextServiceMock } from '../../context/context_service.mock';
2424
import { loggingSystemMock } from '../../logging/logging_system.mock';
2525
import { createHttpServer } from '../test_utils';
26+
import { schema } from '@kbn/config-schema';
2627

2728
let server: HttpService;
2829

@@ -195,6 +196,96 @@ describe('KibanaRequest', () => {
195196
expect(nextSpy).toHaveBeenCalledTimes(0);
196197
expect(completeSpy).toHaveBeenCalledTimes(1);
197198
});
199+
200+
it('does not complete before response has been sent', async () => {
201+
const { server: innerServer, createRouter, registerOnPreAuth } = await server.setup(
202+
setupDeps
203+
);
204+
const router = createRouter('/');
205+
206+
const nextSpy = jest.fn();
207+
const completeSpy = jest.fn();
208+
209+
registerOnPreAuth((req, res, toolkit) => {
210+
req.events.aborted$.subscribe({
211+
next: nextSpy,
212+
complete: completeSpy,
213+
});
214+
return toolkit.next();
215+
});
216+
217+
router.post(
218+
{ path: '/', validate: { body: schema.any() } },
219+
async (context, request, res) => {
220+
expect(completeSpy).not.toHaveBeenCalled();
221+
return res.ok({ body: 'ok' });
222+
}
223+
);
224+
225+
await server.start();
226+
227+
await supertest(innerServer.listener).post('/').send({ data: 'test' }).expect(200);
228+
229+
expect(nextSpy).toHaveBeenCalledTimes(0);
230+
expect(completeSpy).toHaveBeenCalledTimes(1);
231+
});
232+
});
233+
234+
describe('completed$', () => {
235+
it('emits once and completes when response is sent', async () => {
236+
const { server: innerServer, createRouter } = await server.setup(setupDeps);
237+
const router = createRouter('/');
238+
239+
const nextSpy = jest.fn();
240+
const completeSpy = jest.fn();
241+
242+
router.get({ path: '/', validate: false }, async (context, req, res) => {
243+
req.events.completed$.subscribe({
244+
next: nextSpy,
245+
complete: completeSpy,
246+
});
247+
248+
expect(nextSpy).not.toHaveBeenCalled();
249+
expect(completeSpy).not.toHaveBeenCalled();
250+
return res.ok({ body: 'ok' });
251+
});
252+
253+
await server.start();
254+
255+
await supertest(innerServer.listener).get('/').expect(200);
256+
expect(nextSpy).toHaveBeenCalledTimes(1);
257+
expect(completeSpy).toHaveBeenCalledTimes(1);
258+
});
259+
260+
it('emits once and completes when response is aborted', async (done) => {
261+
expect.assertions(2);
262+
const { server: innerServer, createRouter } = await server.setup(setupDeps);
263+
const router = createRouter('/');
264+
265+
const nextSpy = jest.fn();
266+
267+
router.get({ path: '/', validate: false }, async (context, req, res) => {
268+
req.events.completed$.subscribe({
269+
next: nextSpy,
270+
complete: () => {
271+
expect(nextSpy).toHaveBeenCalledTimes(1);
272+
done();
273+
},
274+
});
275+
276+
expect(nextSpy).not.toHaveBeenCalled();
277+
await delay(30000);
278+
return res.ok({ body: 'ok' });
279+
});
280+
281+
await server.start();
282+
283+
const incomingRequest = supertest(innerServer.listener)
284+
.get('/')
285+
// end required to send request
286+
.end();
287+
setTimeout(() => incomingRequest.abort(), 50);
288+
});
198289
});
199290
});
200291
});

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,16 @@ export interface KibanaRequestEvents {
6464
* Observable that emits once if and when the request has been aborted.
6565
*/
6666
aborted$: Observable<void>;
67+
68+
/**
69+
* Observable that emits once if and when the request has been completely handled.
70+
*
71+
* @remarks
72+
* The request may be considered completed if:
73+
* - A response has been sent to the client; or
74+
* - The request was aborted.
75+
*/
76+
completed$: Observable<void>;
6777
}
6878

6979
/**
@@ -186,11 +196,16 @@ export class KibanaRequest<
186196

187197
private getEvents(request: Request): KibanaRequestEvents {
188198
const finish$ = merge(
189-
fromEvent(request.raw.req, 'end'), // all data consumed
199+
fromEvent(request.raw.res, 'finish'), // Response has been sent
190200
fromEvent(request.raw.req, 'close') // connection was closed
191201
).pipe(shareReplay(1), first());
202+
203+
const aborted$ = fromEvent<void>(request.raw.req, 'aborted').pipe(first(), takeUntil(finish$));
204+
const completed$ = merge<void, void>(finish$, aborted$).pipe(shareReplay(1), first());
205+
192206
return {
193-
aborted$: fromEvent<void>(request.raw.req, 'aborted').pipe(first(), takeUntil(finish$)),
207+
aborted$,
208+
completed$,
194209
} as const;
195210
}
196211

src/core/server/server.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -974,6 +974,7 @@ export class KibanaRequest<Params = unknown, Query = unknown, Body = unknown, Me
974974
// @public
975975
export interface KibanaRequestEvents {
976976
aborted$: Observable<void>;
977+
completed$: Observable<void>;
977978
}
978979

979980
// @public

0 commit comments

Comments
 (0)