使用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'); } };
问题根源
- 前后端字段不匹配:前端将所有文件统一追加到
files字段,但后端s3UploadHandler只处理指定的5个文件字段,未匹配的files字段会触发解析逻辑异常。 - 流重复读取:
s3UploadHandler中同时将data(AsyncIterable)传入S3上传的Body,又调用writeAsyncIterableToWritable重复读取流,导致流提前耗尽,引发FormData解析失败。 - 上传处理器逻辑冲突:
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
相关产品推荐
相关产品推荐

