使用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
相关产品推荐
相关产品推荐

