fix(notifications): prevent self-container stack routing (#1242)

* fix(notifications): prevent self-container stack routing

* fix(stack-files): stabilize download metrics in CI
This commit is contained in:
Anso
2026-05-28 12:45:10 -04:00
committed by GitHub
parent e57dbb8da7
commit 265fece988
9 changed files with 704 additions and 53 deletions
+38 -17
View File
@@ -1459,33 +1459,54 @@ stacksRouter.get('/:stackName/files/download', async (req: Request, res: Respons
const encodedFilename = encodeURIComponent(result.filename);
const safeFilename = result.filename.replace(/[\\"]/g, '');
res.setHeader('Content-Disposition', `attachment; filename="${safeFilename}"; filename*=UTF-8''${encodedFilename}`);
// Track download completion off the file stream's lifecycle, not the
// response's. Node's `res.on('finish')` and `res.on('close')` fire in
// platform-dependent order under the in-process supertest transport
// (close occasionally precedes finish, which made the success-vs-error
// recorder racy in CI). The file stream's `end`, `error`, and `close`
// events ARE deterministic: a clean read emits `end` then `close`; a
// destroy emits `close` without `end`; a disk error emits `error` then
// `close`. Hanging the recorder off these signals removes the race.
// Track normal download completion off the file stream's lifecycle, not
// the request lifecycle. Under the in-process supertest transport, request
// close events can race ahead of normal stream completion; response close
// is handled below only as delayed abort cleanup.
// End/error are the durable completion signals, and close can still count
// as success only when the stream reports it read the full file.
let downloadRecorded = false;
const streamWithBytes = result.stream as typeof result.stream & { bytesRead?: number };
const hasReadFullFile = (): boolean => (
result.size === 0 ||
(typeof streamWithBytes.bytesRead === 'number' && streamWithBytes.bytesRead >= result.size)
);
let abortCleanupHandle: NodeJS.Immediate | null = null;
const clearAbortCleanup = (): void => {
if (!abortCleanupHandle) return;
clearImmediate(abortCleanupHandle);
abortCleanupHandle = null;
};
const recordDownloadOnce = (ok: boolean): void => {
if (downloadRecorded) return;
downloadRecorded = true;
clearAbortCleanup();
recordFileOp(req.nodeId, 'download', startedAt, ok);
};
result.stream.on('error', (streamErr) => {
console.error('[files] stream error:', sanitizeForLog(getErrorMessage(streamErr, 'unknown')));
if (!res.headersSent) res.status(500).end();
else res.destroy();
if (!res.headersSent) {
res.removeHeader('Content-Length');
res.status(500).end();
} else {
res.destroy();
}
recordDownloadOnce(false);
});
result.stream.on('end', () => recordDownloadOnce(true));
result.stream.on('close', () => {
if (res.writableEnded) recordDownloadOnce(true);
else recordDownloadOnce(false);
});
req.on('close', () => {
if (!res.writableEnded) result.stream.destroy();
result.stream.on('end', () => recordDownloadOnce(hasReadFullFile()));
result.stream.on('close', () => recordDownloadOnce(hasReadFullFile()));
res.on('close', () => {
if (downloadRecorded || hasReadFullFile()) return;
// At this point, response close is treated as an abort signal. It can
// beat the source stream's final events in the in-process test transport.
// Give a same-turn clean source completion a chance to win before cleanup.
abortCleanupHandle = setImmediate(() => {
abortCleanupHandle = null;
if (downloadRecorded || hasReadFullFile()) return;
recordDownloadOnce(false);
result.stream.destroy();
});
abortCleanupHandle.unref?.();
});
logFileDiag('download stream opened', { stackName, relPath, nodeId: req.nodeId, size: result.size, elapsedMs: Date.now() - startedAt });
result.stream.pipe(res);