如何将S3存储桶文件流式传输至WhatsApp Cloud API /media端点(无本地存储)
如何直接将S3存储桶中的文件发送至WhatsApp云API的/media端点(无需本地存储)
问题背景
我需要把S3存储桶里的文件直接上传到WhatsApp云API的/media端点,不能先保存到本地服务器(服务器无本地存储可用)。目前用本地文件通过fs.createReadStream能成功完成上传,但换成S3的getObject().createReadStream()时,请求会返回500内部错误,其他请求参数确认无误。
现有代码
获取S3流:
const stream = s3.getObject({ Bucket: this.bucketName, Key: fileNameWithFullPath }).createReadStream()
上传请求代码:
const formdata = new FormData(); formdata.append("file", stream, { contentType: "application/pdf" }); formdata.append("messaging_product", "whatsapp"); try { const { data: { id: mediaId }, } = await axios.post( `https://graph.facebook.com/${this.graphAPIVersion}/${this.senderPhoneNumberId}/media`, formdata, { headers: { Authorization: `Bearer ${this.accessToken}`, ...formdata.getHeaders(), }, } ); console.log("Success: ", mediaId); } catch (error) { console.error("Error: ", error); }
返回错误信息
error: { statusCode: 500, statusMessage: 'Internal Server Error', message: 'An unknown error has occurred.', type: 'OAuthException', code: 1, fbtrace_id: 'AxiCaSoItKscIF1qVgy3bej' }
解决方法
1. 给FormData的file字段添加文件名参数
WhatsApp API需要明确的文件名才能正确处理文件,本地流会自动携带文件名,但S3流没有。修改append的第三个参数,补充filename:
// 从S3对象的Key中提取文件名(取路径最后一段) const filename = fileNameWithFullPath.split('/').pop(); formdata.append("file", stream, { contentType: "application/pdf", filename: filename });
2. 从S3元数据获取真实的Content-Type
不要硬编码文件类型,从S3对象的元数据中获取实际的Content-Type,避免类型不匹配:
// 先获取S3对象的元数据 const { ContentType } = await s3.headObject({ Bucket: this.bucketName, Key: fileNameWithFullPath }).promise(); const stream = s3.getObject({ Bucket: this.bucketName, Key: fileNameWithFullPath }).createReadStream(); const filename = fileNameWithFullPath.split('/').pop(); formdata.append("file", stream, { contentType: ContentType, filename: filename });
3. 等待S3流就绪后再发送请求
S3的远程流可能需要时间准备,直接添加到FormData可能导致流未就绪就被发送,监听ready事件后再执行上传:
const stream = s3.getObject({ Bucket: this.bucketName, Key: fileNameWithFullPath }).createReadStream(); // 用Promise包裹,等待流就绪 return new Promise((resolve, reject) => { stream.on('ready', async () => { const formdata = new FormData(); const filename = fileNameWithFullPath.split('/').pop(); formdata.append("file", stream, { contentType: "application/pdf", filename: filename }); formdata.append("messaging_product", "whatsapp"); try { const { data: { id: mediaId } } = await axios.post( `https://graph.facebook.com/${this.graphAPIVersion}/${this.senderPhoneNumberId}/media`, formdata, { headers: { Authorization: `Bearer ${this.accessToken}`, ...formdata.getHeaders(), }, } ); console.log("Success: ", mediaId); resolve(mediaId); } catch (error) { console.error("Error: ", error); reject(error); } }); stream.on('error', (err) => { console.error("Stream error: ", err); reject(err); }); });
4. 添加Content-Length请求头
有些情况下,FormData无法自动获取远程流的内容长度,导致API解析失败。可以从S3元数据中获取ContentLength并添加到请求头:
const { ContentType, ContentLength } = await s3.headObject({ Bucket: this.bucketName, Key: fileNameWithFullPath }).promise(); const stream = s3.getObject({ Bucket: this.bucketName, Key: fileNameWithFullPath }).createReadStream(); const formdata = new FormData(); const filename = fileNameWithFullPath.split('/').pop(); formdata.append("file", stream, { contentType: ContentType, filename: filename }); formdata.append("messaging_product", "whatsapp"); try { const { data: { id: mediaId } } = await axios.post( `https://graph.facebook.com/${this.graphAPIVersion}/${this.senderPhoneNumberId}/media`, formdata, { headers: { Authorization: `Bearer ${this.accessToken}`, ...formdata.getHeaders(), 'Content-Length': ContentLength // 显式添加内容长度 }, // 禁用axios的内容长度限制 maxContentLength: Infinity, maxBodyLength: Infinity } ); console.log("Success: ", mediaId); } catch (error) { console.error("Error: ", error); }
原因总结
本地文件流会自动携带文件名、文件大小等元信息,而S3的远程流默认缺少这些数据。WhatsApp API的/media端点依赖这些元数据解析文件,缺失时就会抛出500内部错误。
内容的提问来源于stack exchange,提问作者theGateway
相关产品推荐
相关产品推荐

