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 = ({
res?: NextApiResponse;
}) => {
let clientAborted = false;
let responseCompleted = !!(res?.writableEnded || res?.writableFinished);
const responseFinished = () => !!(res?.writableEnded || res?.writableFinished);
const responseFinished = () =>
responseCompleted || !!(res?.writableEnded || res?.writableFinished);
const responseWritableAborted = () =>
!!(res as ResponseWithWritableAborted | undefined)?.writableAborted;
const hasExplicitAbort = () => !!(req?.aborted || responseWritableAborted());
const hasBrokenConnection = () => !!(req?.socket?.destroyed || res?.destroyed || res?.errored);
const isAbortedSnapshot = () => {
if (responseFinished()) return false;
return !!(
req?.aborted ||
req?.socket?.destroyed ||
res?.closed ||
res?.destroyed ||
responseWritableAborted() ||
res?.errored
);
return hasExplicitAbort() || hasBrokenConnection();
};
const markResponseCompleted = () => {
responseCompleted = true;
};
const markClientAborted = () => {
if (!responseFinished()) {
clientAborted = true;
}
};
const markClientAbortedIfConnectionBroken = () => {
if (!responseFinished() && (hasExplicitAbort() || hasBrokenConnection())) {
clientAborted = true;
}
};
req?.on('aborted', markClientAborted);
req?.socket?.on('close', markClientAborted);
res?.on('close', markClientAborted);
// close itself is too broad; only stop when paired with explicit abort or a broken connection.
req?.socket?.on('close', markClientAbortedIfConnectionBroken);
res?.on('finish', markResponseCompleted);
res?.on('close', markClientAbortedIfConnectionBroken);
res?.on('error', markClientAborted);
return {
isClientAborted: () => clientAborted || isAbortedSnapshot(),
cleanup: () => {
req?.off('aborted', markClientAborted);
req?.socket?.off('close', markClientAborted);
res?.off('close', markClientAborted);
req?.socket?.off('close', markClientAbortedIfConnectionBroken);
res?.off('finish', markResponseCompleted);
res?.off('close', markClientAbortedIfConnectionBroken);
res?.off('error', markClientAborted);
}
};
......
......@@ -40,30 +40,88 @@ describe('createClientAbortTracker', () => {
tracker.cleanup();
});
it('响应未结束时 close,应判定为客户端 abort', () => {
it('响应未结束但只有 close,不应判定为客户端 abort', () => {
const req = mockReq();
const res = mockRes();
const tracker = createClientAbortTracker({ req, res });
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);
tracker.cleanup();
});
it('socket 在响应结束前关闭,应判定为客户端 abort', () => {
it('socket 单独关闭不应判定为当前请求 abort', () => {
const req = mockReq();
const res = mockRes();
const tracker = createClientAbortTracker({ req, res });
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);
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();
req.socket.destroyed = true;
const res = mockRes({ closed: true });
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