Playwright FormData:用 APIRequestContext 构造 multipart/form-data 请求的完整指南
【免费下载链接】playwrightPlaywright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.项目地址: https://gitcode.com/GitHub_Trending/pl/playwright
本文基于 Playwright 官方 API 文档docs/src/api/class-formdata.md,系统讲解FormData类的定位、set/append/create方法语义、文件字段(Path 与 FilePayload)的三种传值方式,并结合 server/formData.ts 与 client/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)三种语言的关键差异:
- Java:
FormData是独立的 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 的本质区别
set与append的区别在于同名 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同样支持Path与FilePayload两种文件值形态:
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)参数说明
set与append的签名一致(以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 的分支处理),form和multipart两个参数走不同分支:
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.name、file.type作为 MIME、file.arrayBuffer()读取内容作为 buffer);普通对象值则经toFormField()做 Path/FilePayload 到ServerFilePayload的转换;- 最终
formData与multipartData随this._channel.fetch(...)一起经内部协议发送到 server 端执行实际请求。
类型定义层面,types.d.ts 中APIRequestContext.fetch/post/get/...等方法的签名均声明为form?: { [key: string]: string|number|boolean; } | FormData与multipart?: 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 参数如何选择
结合文档与源码可以归纳出清晰的选型规则:
- 纯文本/数字/布尔字段(如登录、搜索、JSON 化的表单提交):使用
form参数。Java 端对应RequestOptions.setForm(...),Python 端对应form=,.NET 端对应Form属性; - 含文件上传(图片、CSV、附件):使用
multipart参数(.NET 为Multipart,Python 为multipart=),文件值可用路径(文件名与 MIME 自动推断)或显式 FilePayload(name/mimeType/buffer 全指定); - 同名字段多值:必须使用
append而非set; - 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” 这类报错时快速定位到原因。
【免费下载链接】playwrightPlaywright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.项目地址: https://gitcode.com/GitHub_Trending/pl/playwright
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考