首页
/ Playwright FormData:用 APIRequestContext 构造 multipart/form-data 请求的完整指南

Playwright FormData:用 APIRequestContext 构造 multipart/form-data 请求的完整指南

2026-09-04 09:26:07作者:乔或婵

本文基于 Playwright 官方 API 文档 docs/src/api/class-formdata.md,系统讲解 FormData 类的定位、set/append/create 方法语义、文件字段(Path 与 FilePayload)的三种传值方式,并结合 server/formData.tsclient/fetch.ts 的源码,深入解析 multipart 请求体在 Playwright 内部的编码流程与参数流转机制。读完后,你可以在 Java、.NET、Python(以及 JavaScript 原生 FormData)环境下正确构造含文件上传的 API 请求,并理解 boundary、Content-Type 推断等底层细节。

什么是 FormData,适用哪些语言

FormData 用于创建通过 APIRequestContext 发送的表单数据。该 API 自 v1.18 引入,API 参考文档面向 Java、C#(.NET)、Python 三种语言(文档元数据标注 langs: java, csharp, python)。在 JavaScript/TypeScript 生态中,Playwright 则直接使用 Node.js 全局的原生 FormData(需要 Node 18+),下文源码分析部分会说明两者的衔接方式。

最典型的用法是构造一个表单对象,然后作为 form 参数传给 page.request().post()(或 .NET 的 Multipart、Python 的 multipart=):

// Java
import com.microsoft.playwright.options.FormData;
FormData form = FormData.create()
    .set("firstName", "John")
    .set("lastName", "Doe")
    .set("age", 30);
page.request().post("http://localhost/submit", RequestOptions.create().setForm(form));
# Python(async / sync 版本调用方式相同,仅 await 差异)
form = FormData()
form.set("firstName", "John")
form.set("lastName", "Doe")
form.set("age", 30)
await page.request.post("http://localhost/submit", form=form)

三种语言的关键差异:

  • JavaFormData 是独立的 options 类,通过 FormData.create() 创建,链式调用返回自身(方法返回类型为 <FormData>),最终以 RequestOptions.create().setForm(form) 传入;
  • .NET:通过 Context.APIRequest.CreateFormData()(或 Page.APIRequest.CreateFormData())获取对象,以 new() { Multipart = multipart } 传入;
  • Python:直接 FormData() 构造,作为关键字参数 form=(仅文本字段)或 multipart=(含文件字段)传入。

核心方法:set、append 与 create

方法 引入版本 语言 返回 语义
FormData.set(name, value) v1.18 Java / C# / Python FormData 设置字段;若同名 key 已存在,覆盖旧值
FormData.append(name, value) v1.44 Java / C# / Python FormData 追加字段;若同名 key 已存在,追加到已有值集合末尾,支持多值字段
FormData.create() v1.18 仅 Java FormData 工厂方法,创建新的 FormData 实例

set 与 append 的本质区别

setappend 的区别在于同名 key 的冲突处理:set 会用新值覆盖该 key 下的所有旧值;append 则把新值追加到既有值序列的末尾。因此需要提交同名字段(例如多个同名附件、复选框数组)时必须使用 append

append 的官方示例(覆盖三种值形态):

FormData form = FormData.create()
    // 仅设置 name 和 value(纯文本字段)。
    .append("firstName", "John")
    // name 和 value 已设置,filename 与 Content-Type 从文件路径推断。
    .append("attachment", Paths.get("pic.jpg"))
    // name、value、filename 与 Content-Type 全部显式设置。
    .append("attachment", new FilePayload("table.csv", "text/csv", Files.readAllBytes(Paths.get("my-tble.csv"))));
page.request().post("http://localhost/submit", RequestOptions.create().setForm(form));
form = FormData()
# 仅设置 name 和 value。
form.append("firstName", "John")
# name 和 value 已设置,filename 与 Content-Type 从文件路径推断。
form.append("attachment", Path("pic.jpg"))
# name、value、filename 与 Content-Type 全部显式设置。
form.append("attachment", {
    "name": "table.csv",
    "mimeType": "text/csv",
    "buffer": Path("my-table.csv").read_bytes(),
})
await page.request.post("http://localhost/submit", multipart=form)
var multipart = Context.APIRequest.CreateFormData();
// 仅设置 name 和 value。
multipart.Append("firstName", "John");
// name、value、filename 与 Content-Type 全部显式设置。
multipart.Append("attachment", new FilePayload()
{
    Name = "pic.jpg",
    MimeType = "image/jpeg",
    Buffer = File.ReadAllBytes("john.jpg")
});
// 同名 attachment 追加第二个文件。
multipart.Append("attachment", new FilePayload()
{
    Name = "table.csv",
    MimeType = "text/csv",
    Buffer = File.ReadAllBytes("my-tble.csv")
});
await Page.APIRequest.PostAsync("https://localhost/submit", new() { Multipart = multipart });

set 的完整示例

set 同样支持 PathFilePayload 两种文件值形态:

FormData form = FormData.create()
    // 仅 name 和 value。
    .set("firstName", "John")
    // filename 与 Content-Type 从文件路径推断。
    .set("profilePicture1", Paths.get("john.jpg"))
    // 四项全部显式设置。
    .set("profilePicture2", new FilePayload("john.jpg", "image/jpeg", Files.readAllBytes(Paths.get("john.jpg"))))
    .set("age", 30);
page.request().post("http://localhost/submit", RequestOptions.create().setForm(form));
var multipart = Context.APIRequest.CreateFormData();
multipart.Set("firstName", "John");
multipart.Set("profilePicture", new FilePayload()
{
    Name = "john.jpg",
    MimeType = "image/jpeg",
    Buffer = File.ReadAllBytes("john.jpg")
});
multipart.Set("age", 30);
await Page.APIRequest.PostAsync("https://localhost/submit", new() { Multipart = multipart });
form = FormData()
form.set("firstName", "John")
form.set("profilePicture1", Path("john.jpg"))
form.set("profilePicture2", {
    "name": "john.jpg",
    "mimeType": "image/jpeg",
    "buffer": Path("john.jpg").read_bytes(),
})
form.set("age", 30)
await page.request.post("http://localhost/submit", multipart=form)

参数说明

setappend 的签名一致(以 set 为例,append原始文档):

参数 类型 说明
name string 字段名
value string | boolean | int | Path | Object(别名 FilePayload 字段值。文本/布尔/数字直接作为字段值;Path 时文件名与 MIME 类型自动推断;FilePayload 对象需包含三个属性:name(文件名,string)、mimeType(文件类型,string)、buffer(文件内容,Buffer)

注意 .NET 的参数签名中 value 不含 Path 形态(官方文档为 .NET 单独标注了参数列表),即 C# 端文件值必须通过显式 FilePayload 对象传入。

源码解析:multipart 请求体如何被编码

服务端:手工拼接 multipart/form-data 字节流

Playwright 并不依赖宿主语言的 multipart 编码库,而是在 server 端自行实现了 MultipartFormData。其核心行为:

  • boundary 生成:构造时调用 generateUniqueBoundaryString()formData.ts#L86-L91),从预定义的 64 个字母数字字符映射表中随机取 16 个字符,拼成 ----WebKitFormBoundary 前缀的 boundary 串(源码注释指明与 WebKit 中的同名实现保持一致);
  • Content-Type 请求头contentTypeHeader() 返回 multipart/form-data; boundary=<boundary>
  • 文本字段addField):写入 content-disposition: form-data; name="..." 头,随后是字段值;
  • 文件字段addFileField):在 content-disposition 头后追加 ; filename="<name>"content-type: <mimeType> 头。其中 MIME 类型的推断逻辑为:优先使用显式传入的 mimeType,否则用 mime 库按文件名推断,再否则回退到 application/octet-stream——这正对应了文档中“filename 和 Content-Type 从文件路径推断”的行为;
  • finish():补上结尾的 --boundary-- 并合并所有 Buffer 片段,产出最终请求体。

客户端:form 与 multipart 参数的分流

client/fetch.ts_fetch 逻辑中(约 L46-L47 的选项定义与 L198-L245 的分支处理),formmultipart 两个参数走不同分支:

  • form 分支:若传入的是原生 globalThis.FormData(Node 18+ 可用),逐项 entries() 取出;此时若遇到非字符串值(即 File),会主动抛出 Expected string for options.form["<name>"], found File. Please use options.multipart instead.——从源码结构看,form 参数语义上只接受纯文本字段,文件字段必须改走 multipart;否则把普通对象转成 {name, value} 键值数组发给 server;
  • multipart 分支:同样支持原生 FormData——字符串值直接透传,File 值则转换为 ServerFilePayload 结构(取 file.namefile.type 作为 MIME、file.arrayBuffer() 读取内容作为 buffer);普通对象值则经 toFormField() 做 Path/FilePayload 到 ServerFilePayload 的转换;
  • 最终 formDatamultipartDatathis._channel.fetch(...) 一起经内部协议发送到 server 端执行实际请求。

类型定义层面,types.d.tsAPIRequestContext.fetch/post/get/... 等方法的签名均声明为 form?: { [key: string]: string|number|boolean; } | FormDatamultipart?: FormData | { [key: string]: string|number|boolean|ReadStream|FilePayload; },即 JS 端原生 FormData 与键值对象两种写法等价。

测试用例印证

tests/library/browsercontext-fetch.spec.ts 中存在针对原生 FormData 的测试,并带有版本守护:it.skip(nodeVersion.major < 20, 'File is not available in Node.js < 20. FormData is not available in Node.js < 18')。从测试结构看,JS 端使用全局 FormData 上传文件至少要求 Node 18(FormData 全局可用),而完整行为验证在 Node 20+ 环境执行——这与上文客户端源码中 globalThis.FormData instanceof 的运行时探测逻辑相互印证。

form 与 multipart 参数如何选择

结合文档与源码可以归纳出清晰的选型规则:

  1. 纯文本/数字/布尔字段(如登录、搜索、JSON 化的表单提交):使用 form 参数。Java 端对应 RequestOptions.setForm(...),Python 端对应 form=,.NET 端对应 Form 属性;
  2. 含文件上传(图片、CSV、附件):使用 multipart 参数(.NET 为 Multipart,Python 为 multipart=),文件值可用路径(文件名与 MIME 自动推断)或显式 FilePayload(name/mimeType/buffer 全指定);
  3. 同名字段多值:必须使用 append 而非 set
  4. JavaScript/TypeScript 用户:不需要 FormData 类的语言绑定,直接 new globalThis.FormData()(Node 18+),form.append('key', fs.createReadStream('a.txt')) 即可,与本文 multipart 分支的源码处理路径一致。

小结

FormData 是 Playwright API Testing 体系中处理 multipart/form-data 请求的专用容器:v1.18 引入 set/create,v1.44 补充 append 以支持同名多值字段;文件字段支持“路径推断”与“FilePayload 显式指定”两种形态,其 MIME 推断行为(显式 mimeType → mime 库按扩展名推断 → application/octet-stream 回退)可以在 server/formData.ts 中逐行验证。理解了客户端 form/multipart 分流与原生 FormData 适配(client/fetch.ts)之后,你就能在多语言环境下正确构造文件上传请求,并在遇到 “Please use options.multipart instead” 这类报错时快速定位到原因。

登录后查看全文
热门项目推荐
相关项目推荐