如何使用AWS SDK v3通过Express从S3正确提供图片/视频资源
问题根源
核心错误是对二进制文件做了UTF-8字符串编码转换。
图片、视频属于二进制格式资源,不存在UTF-8文本编码逻辑,代码里调用Buffer.concat(chunks).toString('utf-8')的操作,会直接篡改原始文件的字节序列,不管是写入本地磁盘还是通过接口返回,拿到的都是被编码破坏的内容,自然无法正常打开。
除此之外代码还有几个可优化的问题:
- 每次请求都新建S3Client实例,会产生不必要的连接开销
- 路由中硬编码
Content-Type为image/jpeg,返回png、mp4等其他格式资源时会出现类型不匹配 - 手动拼接全量Buffer到内存再返回,对大体积视频文件不友好,内存占用高,也无法很好支持视频拖动进度条的范围请求。
修复实现
1. 修正类型定义
把存储文件内容的字段类型从string改为Buffer,保留原始二进制字节:
type S3File = { file: Buffer; type: string; length: number; }
2. 修正S3文件拉取逻辑
全局复用S3Client实例,去掉二进制转UTF-8字符串的错误操作:
// S3客户端全局初始化一次即可,无需每次请求新建 const s3Client = new S3Client({ region: 'eu-west-1', credentials: { accessKeyId: process.env.AWS_KEY_ID as string, secretAccessKey: process.env.AWS_SECRET_KEY as string, }, }); const bucketName = process.env.AWS_BUCKET_NAME as string; export async function getObject(key: string): Promise<S3File> { const response = await s3Client.send( new GetObjectCommand({ Bucket: bucketName, Key: key, }), ); return new Promise((resolve, reject) => { if (!response.Body) { reject(new Error('Empty response body from S3')); return; } const chunks: Buffer[] = []; const bodyStream = response.Body as Readable; bodyStream.once("error", (error) => reject(error)); bodyStream.on('data', (chunk) => chunks.push(Buffer.from(chunk))); bodyStream.on('end', () => resolve({ // 直接拼接Buffer,不做编码转换,保留原始二进制内容 file: Buffer.concat(chunks), type: response.ContentType!, length: response.ContentLength!, }), ); }); }
3. 修正Express路由逻辑
使用S3返回的真实Content-Type,返回原始Buffer内容:
router.get('/media/:id', async (req, res, next) => { try { const file = await getObject(req.params.id); // 调试用写文件逻辑,验证正常后可删除 writeFileSync('./img.jpeg', file.file); // 设置正确响应头,不硬编码资源类型 res.setHeader('Content-Type', file.type); res.setHeader('Content-Length', file.length); res.send(file.file); } catch (err) { // 处理文件不存在场景 if (err.$metadata?.httpStatusCode === 404) { return res.status(404).send({error: 'File not found'}); } next(err); } });
大文件/视频场景优化
如果需要提供大体积视频资源,不需要把整个文件读到内存拼接,可以直接将S3返回的可读流通过管道输出到Express响应,内存占用更低,也能更好适配范围请求:
import { pipeline } from 'stream/promises'; router.get('/media/:id', async (req, res, next) => { try { const response = await s3Client.send( new GetObjectCommand({ Bucket: bucketName, Key: req.params.id, }), ); if (!response.Body) { return res.status(404).send({error: 'File not found'}); } res.setHeader('Content-Type', response.ContentType!); res.setHeader('Content-Length', response.ContentLength!); // 直接流转,无需缓存全量文件 await pipeline(response.Body as Readable, res); } catch (err) { if (err.$metadata?.httpStatusCode === 404) { return res.status(404).send({error: 'File not found'}); } next(err); } });
内容的提问来源于stack exchange,提问作者radmacher
相关产品推荐
相关产品推荐

