fix(backupRoutes): async backup existence check + headersSent guards (#3524)#3529
Conversation
…3524) Harden the system-backup download handler (carried verbatim from server.ts in #3523, now isolated and easy to fix): - Replace the synchronous fs.existsSync (which blocked the event loop in the request path) with an async fsp.access check via a static fs/promises import. - Guard both the archiver 'error' handler and the catch block with res.headersSent — the archive can error / finalize() can throw after archive.pipe(res) has begun streaming, which previously threw 'Cannot set headers after they are sent'. When already streaming, terminate via res.end(). Tests: add a 404 case (missing backup → async access rejects) and a happy-path streaming case (real temp dir → real archiver → 200 tar.gz + audit log). Closes #3524. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @Yeraze's task —— View job PR Review: fix(backupRoutes): async backup existence check + headersSent guards
This PR is well-targeted and fixes two real bugs that would surface under concurrent or error-path conditions. The changes are minimal and focused. Here are my findings: Correctness / Bugs
In A more robust pattern would be to track whether an error response has been sent with a local flag: let errorHandled = false;
archive.on('error', err => {
if (errorHandled) return;
errorHandled = true;
// existing guard logic...
});
// and set errorHandled = true in the catch block tooThis is a minor robustness concern — the
The Code QualityOrdering: headers sent before the audit log, but audit log is before The audit log call at line 232 happens before Dynamic Lines 199–201 use a dynamic SecurityFilename validation is solid. The
PerformanceThe async Test CoverageThe two new tests are well-constructed:
Missing: error path test for the There's no test that exercises the SummaryThe two fixes are correct and solve the stated issues. The |
Summary
Closes #3524. Hardens the system-backup download handler — both issues are pre-existing behaviors carried verbatim from
server.tsintobackupRoutes.tsin #3523, now isolated and fixed.Changes
fs.existsSync(backupPath)(which blocked the event loop in the request path) withawait fsp.access(backupPath)via a staticfs/promisesimport. Missing backup → 404 as before.headersSentguards — the archiver can emiterrormid-stream, andarchive.finalize()can throw, afterarchive.pipe(res)has already sent headers. Both thearchive.on('error')handler and thecatchblock previously calledres.status(500).json(...)unconditionally, throwing "Cannot set headers after they are sent." Now guarded withif (!res.headersSent); when already streaming, the response is terminated withres.end().Tests
Added to
backupRoutes.test.ts:getBackupPath→ a non-existent path, exercising the new asyncfsp.accessrejection.archiver→ 200,Content-Type: application/gzip, correctContent-Disposition, and the audit log. (Closes the download happy-path gap the refactor(server): extract device & system route groups (Refs #3502) #3527 review flagged.)Full suite: 6784 passed, 0 failed. Typecheck + lint clean.
🤖 Generated with Claude Code