Node中HTTP 202响应后重试文件下载的异步问题解决
问题描述
下载每日首次请求报表接口时,若报表正在生成,接口返回HTTP 202状态码。我编写了处理错误、重定向及等待60秒后重试的代码,尽管文件已成功下载且文件流已关闭,但console.log('Downloaded ${reportFileName} successfully.')语句从未执行。需要解决HTTP 202转为200后后续代码无法执行的问题,或获取更优实现方案。
相关代码
// Main function run() async function run() { await getReport() await processReport() } async function getReport() { console.log(`Downloading ${reportFileName}`) await downloadFile(url, reportFileName) console.log(`Downloaded ${reportFileName} successfully.`) // 从未执行 } async function downloadFile (url, targetFile) { return await new Promise((resolve, reject) => { https.get(url, async response => { const code = response.statusCode ?? 0 if (code >= 400) { return reject(new Error(response.statusMessage)) } // 处理重定向 if (code > 300 && code < 400 && !!response.headers.location) { resolve(downloadFile(response.headers.location, targetFile)) return } // 处理报表生成中 if (code == 202) { console.log(`Report: ${reportFileName} is still being generated, trying again in ${timeToSleepMs/1000} seconds...`) await sleep(timeToSleepMs) resolve(downloadFile(url, targetFile)) return } // 创建下载目录(无论是否存在) fs.mkdirSync(outputPath, { recursive: true }, (err) => { if (error) throw error; }); // 保存文件到磁盘 const fileWriter = fs .createWriteStream(`${outputPath}/${targetFile}`) .on('finish', () => { resolve({}) }) response.pipe(fileWriter) }).on('error', error => { reject(error) }) }) } // sleep函数 let timeToSleepMs = (60 * 1000) function sleep(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); }
问题分析与修复方案
1. 同步方法错误使用回调
fs.mkdirSync是同步方法,不接受回调参数,且代码中回调内使用了未定义的error变量(应为err),会触发未捕获的ReferenceError,直接中断Promise链。
修复:移除多余回调,简化为:
fs.mkdirSync(outputPath, { recursive: true });
2. 递归调用未正确等待Promise完成
处理重定向和202状态时,直接resolve(downloadFile(...))会让外层Promise提前resolve到一个未完成的Promise,导致递归链无法串联到最终的文件下载完成事件。
修复:等待递归调用的downloadFile完成后再resolve:
// 处理重定向 if (code > 300 && code < 400 && response.headers.location) { const result = await downloadFile(response.headers.location, targetFile); resolve(result); return; } // 处理202状态 if (code === 202) { console.log(`Report: ${reportFileName} is still being generated, trying again in ${timeToSleepMs/1000} seconds...`); await sleep(timeToSleepMs); const result = await downloadFile(url, targetFile); resolve(result); return; }
3. 未捕获async回调内的错误
https.get的async回调中若抛出错误(如mkdirSync失败),会变成未捕获的Promise rejection,导致Promise链无法正常resolve。
修复:用try/catch包裹回调内所有逻辑,捕获错误后reject给外层Promise:
https.get(url, async (response) => { try { // 原回调内的所有逻辑 } catch (err) { reject(err); } }).on('error', (error) => { reject(error); });
4. 缺失文件写入错误处理
文件写入过程中若发生错误,当前代码未处理,会导致Promise永远无法resolve/reject。
修复:给createWriteStream添加错误监听:
const fileWriter = fs.createWriteStream(`${outputPath}/${targetFile}`); fileWriter.on('finish', () => { resolve({}); }); fileWriter.on('error', (err) => { reject(err); }); response.pipe(fileWriter);
优化后的完整downloadFile函数
async function downloadFile(url, targetFile) { return new Promise((resolve, reject) => { https.get(url, async (response) => { try { const code = response.statusCode ?? 0; if (code >= 400) { return reject(new Error(response.statusMessage)); } // 处理重定向 if (code > 300 && code < 400 && response.headers.location) { const result = await downloadFile(response.headers.location, targetFile); resolve(result); return; } // 处理报表生成中(202状态) if (code === 202) { console.log(`Report: ${reportFileName} is still being generated, trying again in ${timeToSleepMs/1000} seconds...`); await sleep(timeToSleepMs); const result = await downloadFile(url, targetFile); resolve(result); return; } // 创建目录(同步方法,无需回调) fs.mkdirSync(outputPath, { recursive: true }); // 保存文件并处理错误 const fileWriter = fs.createWriteStream(`${outputPath}/${targetFile}`); fileWriter.on('finish', () => { resolve({}); }); fileWriter.on('error', (err) => { reject(new Error(`Failed to write file: ${err.message}`)); }); response.pipe(fileWriter); } catch (err) { reject(new Error(`Request processing failed: ${err.message}`)); } }).on('error', (error) => { reject(new Error(`HTTP request failed: ${error.message}`)); }); }); }
额外优化建议
- 添加重试次数限制:避免无限重试,设置最大重试次数,超过后直接reject。
- 使用
fs.promises.mkdir替代同步方法:避免阻塞事件循环,适合高并发场景。 - 简化Promise嵌套:用async/await重构整个downloadFile函数,减少嵌套层级,提升代码可读性。
内容的提问来源于stack exchange,提问作者TomH
相关产品推荐
相关产品推荐

