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

使用Remix-run与AWS S3上传时遇FormData解析TypeError错误

问题

使用Remix-run的parseMultipartFormData结合自定义上传处理器解析FormData并上传文件到AWS S3时,始终触发错误:TypeError: Could not parse content as FormData.。已确认request对象正确、前端FormData格式无误,且表单已设置multipart/form-data编码类型。相关代码如下:

export const action = async ({request}: ActionArgs) => {
    const composedUploadHandler = composeUploadHandlers(s3UploadHandler, createMemoryUploadHandler());
    const formData = await parseMultipartFormData(request, composedUploadHandler);
    const project: ProjectFormFields = {} as ProjectFormFields;

    const fileFields = [
        'referencesAttachments',
        'documentsAttachments',
        'artworkAttachments',
        'marketingAttachments',
        'investmentAttachments'
    ];

    fileFields.forEach(fileField => {
        const fileJsons = formData.getAll(fileField);
        const files = fileJsons.map(
            (str) => JSON.parse((str as unknown as string) ?? "{}") as {name: string, fileUrl: string}
        );
        project[fileField] = files.map(file => file.fileUrl);
    });

    formData.forEach((value, key) => {
        if (typeof value === 'string' && !fileFields.includes(key)) {
            project[key] = value;
        }
    });

    try {
        const { result, message, status } = await putProjectForm(project);

        return redirect("/", {
            headers: {
                "Set-Cookie": `message=${message}; Path=/;`,
            },
        });

    } catch(error) {
        console.error('There was an error with the form submission', error);
    }

    return null;
};

const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const requiredFields = [
        "projectName",
        "blockchain",
        "supply",
        "mintPrice",
        "mintDate",
        "twitterLink",
        "emailContact",
        "description",
        "teamSummary",
        "teamMembers",
        "references",
        "documents",
        "artwork",
        "marketing",
        "allowlist",
        "investment",
    ];
    const newErrors = {...initialErrors};
    let $form = event.currentTarget;
    let formData = new FormData($form);

    requiredFields.forEach(field => {
        if (!projectForm[field as keyof ProjectFormState]) {
            newErrors[field] = true;
        }
    });

    if (Object.values(newErrors).some(value => value)) {
        setErrors(newErrors);
        return;
    }

    Object.entries(projectForm).forEach(([key, value]) => {
        if (value instanceof File) {
            formData.append('files', value as Blob, value.name);
        } else {
            formData.set(key, value);
        }
    });

    submit(formData, {
        method: $form.getAttribute("method") as HTMLFormMethod ?? $form.method,
        action: $form.getAttribute("action") ?? $form.action,
    });
};

// Initialize S3 client
const s3 = new S3();

// Define the function to stream the upload to S3
const uploadStreamToS3 = ({ Key, Body, Bucket, ContentType }: AWS.S3.Types.PutObjectRequest) => {
    const pass = new PassThrough();
    return {
        writeStream: pass,
        promise: s3.upload({ Bucket, Key, Body: pass, ContentType }).promise(),
    };
};

export const s3UploadHandler: UploadHandler = async ({ name, filename, data, contentType }) => {
    const fileFields = [
        'referencesAttachments',
        'documentsAttachments',
        'artworkAttachments',
        'marketingAttachments',
        'investmentAttachments'
    ];

    if (!fileFields.includes(name)) {
        return undefined;
    }

    const id = uuidv4();
    const filePath = `${id}/${name}`;

    const bucketName = process.env.PDF_BUCKET_NAME;
    if (!bucketName) {
        console.error("Bucket name not defined in environment variables");
        throw new Error("Bucket name not defined in environment variables");
    }

    // Stream upload file to S3
    const stream = uploadStreamToS3({ Key: filePath, Body: data, Bucket: bucketName, ContentType: contentType });
    await writeAsyncIterableToWritable(data, stream.writeStream);
    const uploadedFile = await stream.promise;

    // Public URL to the file
    const fileUrl = uploadedFile.Location;

    // Return a stringified JSON object
    return JSON.stringify({
        name,
        fileUrl
    });
};

export const putProjectForm = async (project: ProjectFormState) => {
    const id = uuidv4();
    const formType = 'project';

    const projectData = {
        id,
        formType,
        ...project
    };

    try {
        await ddb.put({
            TableName: process.env.DYNAMO_TABLE_NAME as string,
            Item: projectData,
        }).promise();

        return { result: 'success', message: 'Data saved successfully', status: 200 };
    } catch (error) {
        console.error(error);
        throw new Error('Error saving data');
    }
};

问题根源

  1. 前后端字段不匹配:前端将所有文件统一追加到files字段,但后端s3UploadHandler只处理指定的5个文件字段,未匹配的files字段会触发解析逻辑异常。
  2. 流重复读取:s3UploadHandler中同时将data(AsyncIterable)传入S3上传的Body,又调用writeAsyncIterableToWritable重复读取流,导致流提前耗尽,引发FormData解析失败。
  3. 上传处理器逻辑冲突:composeUploadHandlers中,s3UploadHandler对不匹配字段返回undefined,交给内存处理器处理时,可能因字段格式问题中断解析流程。

修复方案

1. 修正前端文件字段匹配

确保前端提交的文件字段名与后端处理器预期一致,不要统一用files字段:

// 替换handleSubmit中的文件追加逻辑
Object.entries(projectForm).forEach(([key, value]) => {
    if (value instanceof File) {
        // 使用对应字段名追加文件,而非统一的'files'
        formData.append(key, value as Blob, value.name);
    } else {
        formData.set(key, value);
    }
});

2. 修复S3上传的流读取问题

移除重复的流写入操作,直接使用data作为S3上传的Body:

export const s3UploadHandler: UploadHandler = async ({ name, filename, data, contentType }) => {
    const fileFields = [
        'referencesAttachments',
        'documentsAttachments',
        'artworkAttachments',
        'marketingAttachments',
        'investmentAttachments'
    ];

    if (!fileFields.includes(name)) {
        return undefined;
    }

    const id = uuidv4();
    // 使用filename作为路径一部分,避免同名文件覆盖
    const filePath = `${id}/${filename}`;

    const bucketName = process.env.PDF_BUCKET_NAME;
    if (!bucketName) {
        console.error("Bucket name not defined in environment variables");
        throw new Error("Bucket name not defined in environment variables");
    }

    // 直接传入data作为Body,无需额外流处理
    const uploadedFile = await s3.upload({
        Bucket: bucketName,
        Key: filePath,
        Body: data,
        ContentType: contentType
    }).promise();

    const fileUrl = uploadedFile.Location;

    return JSON.stringify({
        name,
        fileUrl
    });
};

3. 强化错误捕获与调试

在parseMultipartFormData外层添加精准错误捕获,便于定位问题:

export const action = async ({request}: ActionArgs) => {
    try {
        const composedUploadHandler = composeUploadHandlers(s3UploadHandler, createMemoryUploadHandler());
        const formData = await parseMultipartFormData(request, composedUploadHandler);
        
        // 后续表单处理逻辑...
    } catch (error) {
        console.error("FormData解析失败详情:", error);
        return new Response(JSON.stringify({error: "表单解析失败"}), {status: 400});
    }
};

4. 确认表单编码设置

再次检查前端表单的enctype属性是否正确设置:

<form method="post" enctype="multipart/form-data">
    <!-- 表单内容 -->
</form>

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 10:20:32