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

如何将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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 12:05:21