D2: Fix upload retry 500 on missing file — async fire-and-forget

retryJob() now wraps the file read in a fire-and-forget async IIFE using
fs/promises.readFile instead of readFileSync. When the original upload
file has been cleaned up (common after failure), the IIFE catches the
ENOENT error and marks the job as 'failed' with a 'retry failed:' prefix
asynchronously — the endpoint returns 200 immediately.

Includes 4 new integration tests:
- Missing file returns immediately (no throw), job async-fails
- ENOENT message propagated in job.error
- 404 on unknown job still works (unchanged contract)
- File present resets job to pending (runJob triggered)
This commit is contained in:
Giancarmine Salucci
2026-07-08 22:54:47 +02:00
parent 37e877b3c2
commit 4de9390913
3 changed files with 163 additions and 7 deletions
+21 -7
View File
@@ -50,7 +50,11 @@ export async function startUploadJob(
return job.id;
}
/** Retry a failed/cancelled job — YouTube or upload-source. */
/** Retry a failed/cancelled job — YouTube or upload-source.
*
* Upload retry reads the original file asynchronously (fire-and-forget IIFE)
* so a missing file produces an async job failure instead of a synchronous
* 500 crash. The outer function returns immediately. */
export async function retryJob(jobId: string): Promise<void> {
const job = getJob(jobId);
if (!job) throw new Error('Job not found');
@@ -58,12 +62,22 @@ export async function retryJob(jobId: string): Promise<void> {
const isUpload = !job.source.startsWith('http');
if (isUpload) {
const { readFileSync } = await import('fs');
const uploadPath = getUploadPath(jobId, job.source);
const buffer = readFileSync(uploadPath);
runJob(jobId, { type: 'upload', buffer, filename: job.source }, job.audioMode as AudioMode).catch((err) => {
console.error(`[pipeline] retry upload job ${jobId} failed:`, err);
});
// Fire-and-forget: missing file -> async job failure, not 500
(async () => {
try {
const { readFile } = await import('fs/promises');
const uploadPath = getUploadPath(jobId, job.source);
const buffer = await readFile(uploadPath);
runJob(jobId, { type: 'upload', buffer, filename: job.source }, job.audioMode as AudioMode).catch((err) => {
console.error(`[pipeline] retry upload job ${jobId} failed:`, err);
});
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
console.error(`[pipeline] retry upload job ${jobId} failed to read file:`, message);
updateJob({ id: jobId, status: 'failed', error: `retry failed: ${message}` });
emitProgress(jobId, { type: 'error', message: `retry failed: ${message}` });
}
})();
} else {
runJob(jobId, { type: 'youtube', url: job.source }, job.audioMode as AudioMode).catch((err) => {
console.error(`[pipeline] retry job ${jobId} failed:`, err);