Skip to content

Commit e7e7e94

Browse files
committed
Improve Node 16 abort handling further
The previous version could lose requests, where they were never reported with a response or an abort, if the handler failed after closing the writable response. In that case, the error handler fires, but fails to write an error, and so tries to mark the response as aborted. Because the writable is closed, our previous logic assumed that a response was written OK. That's not necessarily true. Now, we instead require that all handlers return ASAP (promises OK, but not IO) after closing the socket, and as long as they do then everything is OK. It doesn't seem to be possible to directly reproduce that error case in the tests here, but the new test at least get closer than the existing ones.
1 parent c668d2c commit e7e7e94

File tree

2 files changed

+65
-5
lines changed

2 files changed

+65
-5
lines changed

src/server/mockttp-server.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -389,16 +389,16 @@ export default class MockttpServer extends AbstractMockttp implements Mockttp {
389389

390390
let result: 'responded' | 'aborted' | null = null;
391391
const abort = () => {
392-
if (result === null && !rawResponse.writableEnded) {
392+
if (result === null) {
393393
result = 'aborted';
394394
request.timingEvents.abortedTimestamp = now();
395395
this.announceAbortAsync(request);
396396
}
397397
}
398398
request.once('aborted', abort);
399-
// In Node 16+ we don't get an abort even in many cases, just closes, but we know
400-
// it's aborted because the response is closed before we've written anything.
401-
rawResponse.once('close', abort);
399+
// In Node 16+ we don't get an abort event in many cases, just closes, but we know
400+
// it's aborted because the response is closed with no other result being set.
401+
rawResponse.once('close', () => setImmediate(abort));
402402

403403
this.announceInitialRequestAsync(request);
404404

test/integration/subscriptions/response-events.spec.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,14 +340,74 @@ describe("Abort subscriptions", () => {
340340
expect(seenAbort.timingEvents.bodyReceivedTimestamp).to.equal(undefined);
341341
expect(wasRequestSeen).to.equal(false);
342342
});
343+
344+
describe("given a server that closes connections", () => {
345+
346+
const badServer = new http.Server((req, res) => {
347+
// Forcefully close the socket with no response
348+
req.socket!.destroy();
349+
});
350+
351+
beforeEach(async () => {
352+
await new Promise((resolve, reject) => {
353+
badServer.listen(8901);
354+
badServer.on('listening', resolve);
355+
badServer.on('error', reject);
356+
});
357+
});
358+
359+
afterEach(() => {
360+
badServer.close();
361+
});
362+
363+
it("should be sent when the remote server aborts the resopnse", async () => {
364+
let seenAbortPromise = getDeferred<InitiatedRequest>();
365+
await server.on('abort', (r) => seenAbortPromise.resolve(r));
366+
367+
let seenResponsePromise = getDeferred<CompletedResponse>();
368+
await server.on('response', (r) => seenResponsePromise.resolve(r));
369+
370+
await server.anyRequest().thenForwardTo(`http://localhost:8901`);
371+
372+
fetch(server.urlFor("/mocked-endpoint")).catch(() => {});
373+
374+
await Promise.race([
375+
seenAbortPromise,
376+
seenResponsePromise.then(() => {
377+
throw new Error('Should not fire a response event');
378+
})
379+
]);
380+
});
381+
382+
it("should be sent when a remote proxy aborts the response", async () => {
383+
let seenAbortPromise = getDeferred<InitiatedRequest>();
384+
await server.on('abort', (r) => seenAbortPromise.resolve(r));
385+
386+
let seenResponsePromise = getDeferred<CompletedResponse>();
387+
await server.on('response', (r) => seenResponsePromise.resolve(r));
388+
389+
await server.anyRequest().thenPassThrough({
390+
proxyConfig: { proxyUrl: `http://localhost:8901` }
391+
});
392+
393+
fetch(server.urlFor("/mocked-endpoint")).catch(() => {});
394+
395+
await Promise.race([
396+
seenAbortPromise,
397+
seenResponsePromise.then(() => {
398+
throw new Error('Should not fire a response event');
399+
})
400+
]);
401+
});
402+
});
343403
});
344404

345405
it("should be sent in place of response notifications, not in addition", async () => {
346406
let seenRequestPromise = getDeferred<CompletedRequest>();
347407
await server.on('request', (r) => seenRequestPromise.resolve(r));
348408

349409
let seenResponsePromise = getDeferred<CompletedResponse>();
350-
await server.on('response', (r) => Promise.resolve(r));
410+
await server.on('response', (r) => seenResponsePromise.resolve(r));
351411

352412
await server.post('/mocked-endpoint').thenCallback((req) => delay(500).then(() => ({})));
353413

0 commit comments

Comments
 (0)