Commit edbf57b2 by Archer Committed by GitHub

fix: avoid client abort close false positives (#6910)

* fix: avoid socket close abort false positives

* fix: require abort signal for close tracking
parent 747c9cad
...@@ -13,39 +13,47 @@ export const createClientAbortTracker = ({ ...@@ -13,39 +13,47 @@ export const createClientAbortTracker = ({
res?: NextApiResponse; res?: NextApiResponse;
}) => { }) => {
let clientAborted = false; let clientAborted = false;
let responseCompleted = !!(res?.writableEnded || res?.writableFinished);
const responseFinished = () => !!(res?.writableEnded || res?.writableFinished); const responseFinished = () =>
responseCompleted || !!(res?.writableEnded || res?.writableFinished);
const responseWritableAborted = () => const responseWritableAborted = () =>
!!(res as ResponseWithWritableAborted | undefined)?.writableAborted; !!(res as ResponseWithWritableAborted | undefined)?.writableAborted;
const hasExplicitAbort = () => !!(req?.aborted || responseWritableAborted());
const hasBrokenConnection = () => !!(req?.socket?.destroyed || res?.destroyed || res?.errored);
const isAbortedSnapshot = () => { const isAbortedSnapshot = () => {
if (responseFinished()) return false; if (responseFinished()) return false;
return !!( return hasExplicitAbort() || hasBrokenConnection();
req?.aborted || };
req?.socket?.destroyed || const markResponseCompleted = () => {
res?.closed || responseCompleted = true;
res?.destroyed ||
responseWritableAborted() ||
res?.errored
);
}; };
const markClientAborted = () => { const markClientAborted = () => {
if (!responseFinished()) { if (!responseFinished()) {
clientAborted = true; clientAborted = true;
} }
}; };
const markClientAbortedIfConnectionBroken = () => {
if (!responseFinished() && (hasExplicitAbort() || hasBrokenConnection())) {
clientAborted = true;
}
};
req?.on('aborted', markClientAborted); req?.on('aborted', markClientAborted);
req?.socket?.on('close', markClientAborted); // close itself is too broad; only stop when paired with explicit abort or a broken connection.
res?.on('close', markClientAborted); req?.socket?.on('close', markClientAbortedIfConnectionBroken);
res?.on('finish', markResponseCompleted);
res?.on('close', markClientAbortedIfConnectionBroken);
res?.on('error', markClientAborted); res?.on('error', markClientAborted);
return { return {
isClientAborted: () => clientAborted || isAbortedSnapshot(), isClientAborted: () => clientAborted || isAbortedSnapshot(),
cleanup: () => { cleanup: () => {
req?.off('aborted', markClientAborted); req?.off('aborted', markClientAborted);
req?.socket?.off('close', markClientAborted); req?.socket?.off('close', markClientAbortedIfConnectionBroken);
res?.off('close', markClientAborted); res?.off('finish', markResponseCompleted);
res?.off('close', markClientAbortedIfConnectionBroken);
res?.off('error', markClientAborted); res?.off('error', markClientAborted);
} }
}; };
......
...@@ -40,30 +40,88 @@ describe('createClientAbortTracker', () => { ...@@ -40,30 +40,88 @@ describe('createClientAbortTracker', () => {
tracker.cleanup(); tracker.cleanup();
}); });
it('响应未结束时 close,应判定为客户端 abort', () => { it('响应未结束但只有 close,不应判定为客户端 abort', () => {
const req = mockReq(); const req = mockReq();
const res = mockRes(); const res = mockRes();
const tracker = createClientAbortTracker({ req, res }); const tracker = createClientAbortTracker({ req, res });
res.emit('close'); res.emit('close');
expect(tracker.isClientAborted()).toBe(false);
tracker.cleanup();
});
it('响应未结束且连接已断开时 close,应判定为客户端 abort', () => {
const req = mockReq();
const res = mockRes();
const tracker = createClientAbortTracker({ req, res });
req.socket.destroyed = true;
res.emit('close');
expect(tracker.isClientAborted()).toBe(true);
tracker.cleanup();
});
it('响应 writableAborted 时 close,应判定为客户端 abort', () => {
const req = mockReq();
const res = mockRes();
const tracker = createClientAbortTracker({ req, res });
res.writableAborted = true;
res.emit('close');
expect(tracker.isClientAborted()).toBe(true); expect(tracker.isClientAborted()).toBe(true);
tracker.cleanup(); tracker.cleanup();
}); });
it('socket 在响应结束前关闭,应判定为客户端 abort', () => { it('socket 单独关闭不应判定为当前请求 abort', () => {
const req = mockReq(); const req = mockReq();
const res = mockRes(); const res = mockRes();
const tracker = createClientAbortTracker({ req, res }); const tracker = createClientAbortTracker({ req, res });
req.socket.emit('close'); req.socket.emit('close');
expect(tracker.isClientAborted()).toBe(false);
tracker.cleanup();
});
it('请求 aborted 时应判定为客户端 abort', () => {
const req = mockReq();
const res = mockRes();
const tracker = createClientAbortTracker({ req, res });
req.emit('aborted');
expect(tracker.isClientAborted()).toBe(true); expect(tracker.isClientAborted()).toBe(true);
tracker.cleanup(); tracker.cleanup();
}); });
it('创建 tracker 前响应已经异常关闭,应通过快照判定为客户端 abort', () => { it('响应 finish 后 close,不应受 closed 状态误判', () => {
const req = mockReq();
const res = mockRes();
const tracker = createClientAbortTracker({ req, res });
res.emit('finish');
res.closed = true;
res.emit('close');
expect(tracker.isClientAborted()).toBe(false);
tracker.cleanup();
});
it('创建 tracker 前只有 closed,不应通过快照判定为客户端 abort', () => {
const req = mockReq();
const res = mockRes({ closed: true });
const tracker = createClientAbortTracker({ req, res });
expect(tracker.isClientAborted()).toBe(false);
tracker.cleanup();
});
it('创建 tracker 前连接已经断开,应通过快照判定为客户端 abort', () => {
const req = mockReq(); const req = mockReq();
req.socket.destroyed = true;
const res = mockRes({ closed: true }); const res = mockRes({ closed: true });
const tracker = createClientAbortTracker({ req, res }); const tracker = createClientAbortTracker({ req, res });
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment