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

Next.js 13 App目录路由下能否流式传输POST文件至外部API?

问题

我尝试从浏览器上传图片,通过后端流式传输至Imgur API,但只有小图片(比如28KB)能正常上传,80KB的图片要花1分钟才崩溃。怀疑是内存问题,想知道能不能用流来解决。


现有代码

route.js(处理form-data POST请求)

import uploadImage from "@/utils/uploadImage";
import { NextResponse } from "next/server";
import path from 'path'

export const POST = async (req) => {
    try {
        const formData = await req.formData();
        const file = formData.get('image');
        const allowedExtensions = ['.jpg', '.jpeg', '.png']
        if (!file || !allowedExtensions.includes(path.extname(file.name))){
            return NextResponse.json({
                ok:false,
                msg: 'Invalid file'
            })
        }
        let newFileURL = await uploadImage(file)
        if (!newFileURL){
            return NextResponse.json({
                ok:false,
                msg: 'Invalid file'
            })
        }
        
        return NextResponse.json({
            ok:true,
            url: newFileURL
        })
        
    } catch (error) {
        console.log(error)
        return new Response(null, {
            status:500
        })
    }
}

uploadImage函数

export default async function uploadImage(fileBlob){
    const postData = new FormData()
    postData.set('image', fileBlob, fileBlob.name)
    let response = await fetch(process.env.IMGUR_UPLOAD_URL, {
        method:'POST',
        body:postData,
        headers:{
            "Authorization": "Client-ID " + process.env.IMGUR_CLIENT_ID
        }
    })
    response = await response.json()
    if (!response.status){
        return null
    }

    
    return response.data.link
}

解决方案:改用流式传输

当前代码会把整个文件解析成Blob存入内存,大文件会导致内存占用过高,引发超时或崩溃。改用流式处理可以避免一次性加载整个文件,直接把浏览器上传的流转发给Imgur。

简化版:直接转发请求流

不需要解析FormData,直接把原始请求流传给Imgur,同时保留必要的请求头:

import { NextResponse } from "next/server";

export const POST = async (req) => {
    try {
        const imgurResponse = await fetch(process.env.IMGUR_UPLOAD_URL, {
            method: 'POST',
            body: req.body, // 直接转发原始请求流
            headers: {
                "Authorization": `Client-ID ${process.env.IMGUR_CLIENT_ID}`,
                // 保留浏览器传来的Content-Type(包含form-data边界)
                "Content-Type": req.headers.get('content-type')
            }
        });

        if (!imgurResponse.ok) {
            return NextResponse.json({ ok: false, msg: '上传到Imgur失败' }, { status: imgurResponse.status });
        }

        const data = await imgurResponse.json();
        return NextResponse.json({ ok: true, url: data.data.link });

    } catch (error) {
        console.log(error);
        return new Response(null, { status: 500 });
    }
};

带文件验证的优化版

如果需要验证文件类型,可通过解析multipart流的头部信息获取文件名,无需加载整个文件:

import { NextResponse } from "next/server";
import path from 'path';

// 简易multipart解析函数,仅提取文件信息和流
async function parseMultipartFile(req) {
    const contentType = req.headers.get('content-type');
    if (!contentType?.startsWith('multipart/form-data')) {
        throw new Error('Invalid content type');
    }

    const boundary = contentType.split('boundary=')[1];
    const reader = req.body.getReader();
    const decoder = new TextDecoder('utf-8');
    let fileName = '';
    let fileStream;

    while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        const chunkStr = decoder.decode(value, { stream: true });
        if (!fileName) {
            // 从头部提取文件名
            const fileNameMatch = chunkStr.match(/filename="([^"]+)"/);
            if (fileNameMatch) fileName = fileNameMatch[1];
        }

        // 找到文件内容起始位置,截断后返回流
        const contentStart = chunkStr.indexOf('\r\n\r\n');
        if (contentStart !== -1) {
            const contentChunk = value.slice(contentStart + 4);
            fileStream = new ReadableStream({
                async start(controller) {
                    controller.enqueue(contentChunk);
                    // 继续读取剩余流
                    while (true) {
                        const { done, value } = await reader.read();
                        if (done) break;
                        // 检查是否到达边界,截断尾部
                        const endIndex = value.indexOf(`--${boundary}`);
                        if (endIndex !== -1) {
                            controller.enqueue(value.slice(0, endIndex));
                            break;
                        }
                        controller.enqueue(value);
                    }
                    controller.close();
                }
            });
            break;
        }
    }

    return { fileName, fileStream };
}

export const POST = async (req) => {
    try {
        const { fileName, fileStream } = await parseMultipartFile(req);
        
        const allowedExtensions = ['.jpg', '.jpeg', '.png'];
        if (!fileName || !allowedExtensions.includes(path.extname(fileName))) {
            return NextResponse.json({ ok: false, msg: 'Invalid file' });
        }

        const formData = new FormData();
        formData.set('image', fileStream, fileName);

        const imgurResponse = await fetch(process.env.IMGUR_UPLOAD_URL, {
            method: 'POST',
            body: formData,
            headers: {
                "Authorization": `Client-ID ${process.env.IMGUR_CLIENT_ID}`
            }
        });

        if (!imgurResponse.ok) {
            return NextResponse.json({ ok: false, msg: '上传到Imgur失败' }, { status: imgurResponse.status });
        }

        const data = await imgurResponse.json();
        return NextResponse.json({ ok: true, url: data.data.link });

    } catch (error) {
        console.log(error);
        return new Response(null, { status: 500 });
    }
};

流式处理的优势

  • 内存占用低:文件分块传输,不会一次性加载整个文件到内存
  • 传输效率高:避免中间内存拷贝,减少超时概率

内容的提问来源于stack exchange,提问作者Rafa0712

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 09:13:45