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

如何使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.03 06:51:38