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

使用Axios处理流式请求时,如何等待所有响应块接收完成?

问题描述

我有一个代理服务器,需要调用外部API将文本合成为语音。API文档说明:请求后会先返回带响应头的信息,之后以流模式返回二进制数据,响应体包含Transfer-Encoding: chunked。

我使用了responseType: 'stream',代码如下:

export const synthesizeVoice = async (contentType: ContentType,
                                      dataForSynthesize: string,
                                      format: string = 'wav16', 
                                      voice:  string = 'Nec_24000') => {
    const token = await authHandler.getAuthData();
    const date  = new Date(); const timestamp = date.getTime();
    const file  = fs.createWriteStream(timestamp + '.wav'); // 需要在服务器存储数据,所以用这个
    
    const response = await axios({
        method: 'POST',
        url: SAPI.SS_SYNTH,

        headers: {
            Authorization:  'Bearer ' + token,
            'Content-Type': (contentType === 'ssml') ? 'application/ssml' : 'application/text',
        },
        params: {
            format: format,
            voice:  voice,
        },
        data: dataForSynthesize,

        responseType: 'stream',
    });
    
    // 将数据写入文件
    response.data.pipe(file);
    // 返回文件路径给上层,以便向前端返回二进制数据
    return file.path;
};

核心问题:方法在收到第一块响应数据后就返回了文件路径,此时文件还未完全写入,导致前端收到不完整的音频文件。

想知道:

  • 如何等待所有二进制数据接收完成后再返回?
  • 是否应该让前端也使用流式请求分块获取数据?
  • 有没有更好的实现方案?

更新:可用代码

axios({
    // 此处无改动,简化示例已隐藏
})
.then(resp => {
    // 文件的事件监听器在我的场景中无效,因此使用Axios响应的事件监听器
    resp.data.pipe(file);

    resp.data.on('end', () => {
        file.close();

        res.set({
            'Content-Type': 'audio/wav16',
        });
        const resFile = fs.createReadStream(file.path).pipe(res);
    });
})
.catch(err => {
    console.log(err.message)
});

解决方案

1. 等待文件写入完成再返回路径

问题根源是pipe是异步操作,直接返回file.path时文件还在写入中。可以把流程包装成Promise,监听流的end和error事件,确保文件写完再返回:

export const synthesizeVoice = async (contentType: ContentType,
                                      dataForSynthesize: string,
                                      format: string = 'wav16', 
                                      voice:  string = 'Nec_24000') => {
    const token = await authHandler.getAuthData();
    const date = new Date(); 
    const timestamp = date.getTime();
    const filePath = `${timestamp}.wav`;
    const file = fs.createWriteStream(filePath);

    const response = await axios({
        method: 'POST',
        url: SAPI.SS_SYNTH,
        headers: {
            Authorization:  'Bearer ' + token,
            'Content-Type': (contentType === 'ssml') ? 'application/ssml' : 'application/text',
        },
        params: {
            format: format,
            voice:  voice,
        },
        data: dataForSynthesize,
        responseType: 'stream',
    });

    return new Promise((resolve, reject) => {
        response.data.pipe(file);
        
        // 监听流结束事件,确认写入完成
        response.data.on('end', () => {
            file.close(err => {
                if (err) reject(err);
                else resolve(filePath);
            });
        });

        // 监听响应流错误,销毁未完成文件
        response.data.on('error', err => {
            file.destroy();
            reject(err);
        });

        // 监听文件写入错误,终止响应流
        file.on('error', err => {
            response.data.destroy();
            reject(err);
        });
    });
};

上层调用synthesizeVoice时,会等待文件完全写入后才拿到路径,前端就能获取完整音频。

2. 直接流式转发给前端(更高效)

如果不需要在服务器持久化音频文件,可跳过本地写入,直接把外部API的响应流转发给前端,既节省磁盘空间,又能让前端更快接收数据:

// 假设这是Express路由处理函数
export const synthesizeAndForward = async (req, res) => {
    const { contentType, dataForSynthesize, format = 'wav16', voice = 'Nec_24000' } = req.body;
    const token = await authHandler.getAuthData();

    try {
        const response = await axios({
            method: 'POST',
            url: SAPI.SS_SYNTH,
            headers: {
                Authorization:  'Bearer ' + token,
                'Content-Type': (contentType === 'ssml') ? 'application/ssml' : 'application/text',
            },
            params: {
                format: format,
                voice:  voice,
            },
            data: dataForSynthesize,
            responseType: 'stream',
        });

        // 转发响应头给前端
        res.set('Content-Type', 'audio/wav16');
        // 直接将响应流pipe到前端响应
        response.data.pipe(res);

        // 处理流错误
        response.data.on('error', err => {
            res.status(500).send('语音合成失败');
        });
    } catch (err) {
        res.status(500).send(err.message);
    }
};

这种方式无需等待整个文件写入,前端会以流式方式接收数据,体验更好,还能避免磁盘IO开销。

3. 前端流式请求处理

如果选择方案2直接转发流,前端可通过fetch配合ReadableStream处理流式响应,示例如下:

async function fetchAudio() {
    const response = await fetch('/synthesize', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ /* 请求参数 */ })
    });

    if (!response.ok) throw new Error('请求失败');

    const reader = response.body.getReader();
    const audioContext = new AudioContext();
    let buffer;

    while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        
        // 解码并播放流式音频
        buffer = await audioContext.decodeAudioData(value);
        const source = audioContext.createBufferSource();
        source.buffer = buffer;
        source.connect(audioContext.destination);
        source.start();
    }
}

但如果业务必须在服务器存储音频文件,还是用方案1等待写入完成后返回完整路径。

内容的提问来源于stack exchange,提问作者Mikhail

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 08:10:37