You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 13:30:53