Playwright Request 类全解:请求生命周期、API 方法与源码实现剖析
【免费下载链接】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 文档中的Request类为骨架,完整讲解页面发起每个网络请求时的事件序列(request/response/requestfinished/requestfailed)、Request类的全部成员方法(URL、方法、请求体、请求头、重定向链、资源类型、计时、体积等),并结合playwright-core中客户端与服务端两层实现源码,解释各方法的底层数据来源、headers()与allHeaders()的差异、以及frame()为何可能在某些场景抛错。读完本文,你可以掌握在自动化与测试中做网络监听、失败请求审计、重定向链路追踪、性能计时采集和请求体解析的完整方案。
一、Request 在页面网络事件流中的位置
Request类自 v1.8 引入。当页面为某个网络资源发起请求时,Playwright 按如下顺序由 Page 发出事件:
page.request—— 请求刚被页面发出时触发,此时拿到的就是Request对象;page.response—— 当(如果)收到该请求的响应状态码与响应头时触发;page.requestfinished—— 响应体下载完毕、请求完整结束时触发;page.requestfailed—— 请求在某一步失败时触发,替代requestfinished(也可能替代response)。
两条容易踩坑的边界规则(来自官方文档原注):
- HTTP 错误码不算失败:404、503 这类 HTTP 错误响应从 HTTP 协议角度看仍是"成功收到响应",请求会以
requestfinished收尾,而不会触发requestfailed。requestfailed只在网络层失败(DNS 解析失败、连接被重置、超时等)时触发,此时request.failure()会返回带errorText的对象; - 重定向会拆分请求:收到 3xx 响应时,当前请求以
requestfinished正常结束,随后浏览器向重定向目标 URL 发出一个新的Request对象,新旧两个请求通过redirectedFrom()/redirectedTo()互相引用。
从源码看,事件序列在服务端 Request 类 中对应三条状态通道:_setFailureText()(L240-L243)写入失败文本并把等待响应的 Promise 以null解决;_setResponse()(L297-L301)挂上响应并 resolve 等待 Promise;构造器中redirectedFrom._redirectedTo = this(L209-L210)则完成了重定向链的双向链接。
二、基础信息读取:url、method、resourceType
最基础的三个方法是url()、method()、resourceType(),均为同步方法:
url()—— 请求的 URL。注意服务端构造时会先 stripFragmentFromUrl 去掉 URL 的#fragment部分,因此url()返回的 URL 不含锚点;method()—— HTTP 方法(GET、POST 等);resourceType()—— 渲染引擎视角的资源类型。
文档列出的resourceType()取值为:document、stylesheet、image、media、font、script、texttrack、xhr、fetch、eventsource、websocket、manifest、other。从 服务端 ResourceType 定义 看,内部类型联合比对外文档更宽,还包括beacon、ping、cspreport等,这些是浏览器的ResourceType枚举值,但 Playwright 对外的文档口径以文档列出的集合为准。
在 客户端 Request 类 中,这三者都直接读取初始化器_initializer中的字段(url()为 L127-L129,resourceType()为 L131-L133,method()为 L135-L137)。有一个值得注意的实现细节:url()和method()会优先返回_fallbackOverrides中的覆盖值——这是route.continue({ url, method })之类的路由重写生效后,Request对象"看到"的是重写后的值,这也是文档中route.continue()改写 URL 后request.url()会变化这一行为的来源。
三、failure():请求失败信息
failure()在请求失败(requestfailed事件)时返回{ errorText }对象,其中errorText是人类可读的错误信息,例如'net::ERR_FAILED';请求未失败时返回null。JS 端返回{ errorText: string },Java / Python / C# 端则直接返回字符串(C# 为Failure属性)。
多语言用法(官方文档示例):
page.on('requestfailed', request => { console.log(request.url() + ' ' + request.failure().errorText); });page.onRequestFailed(request -> { System.out.println(request.url() + " " + request.failure()); });page.on("requestfailed", lambda request: print(request.url + " " + request.failure))page.RequestFailed += (_, request) => { Console.WriteLine(request.Failure); };实现上,客户端 failure() 只是把内部_failureText包装成{ errorText }返回;而_failureText的赋值发生在服务端 Request._setFailureText(),它同时把"等待响应"的 Promise 以null解决——这解释了为什么失败请求调用request.response()会拿到null而不是挂起:响应等待与失败事件在内部是同一条竞态通道。
四、frame()、serviceWorker() 与 isNavigationRequest():请求的"发起者"
frame()返回发起该请求的 Frame。文档明确提醒:部分场景下 frame 不可用,直接调用frame()会抛错,需要先用其他方法判别:
- 请求源自 Service Worker —— 此时没有关联 Frame,用
request.serviceWorker()检查; - 导航请求在对应 Frame 创建之前发出 —— 用
request.isNavigationRequest()检查。
官方给出的全覆盖判断示例:
if (request.serviceWorker()) console.log(`request ${request.url()} from a service worker`); else if (request.isNavigationRequest()) console.log(`request ${request.url()} is a navigation request`); else console.log(`request ${request.url()} from a frame ${request.frame().url()}`);String frameUrl = request.frame().url();frame_url = request.frame.urlvar frameUrl = request.Frame.Url;抛错行为在 客户端 frame() 实现 中可以直接看到:_initializer.frame为空时断言当前请求必须有serviceWorker(),否则抛出Service Worker requests do not have an associated frame.;frame 存在但尚未挂到 Page(导航请求先于 Frame 创建)时,抛出包含"the request was issued before the frame is created"指引的错误信息,提示用户调用isNavigationRequest()判别。
serviceWorker()(since v1.24,JS/Python 可用)返回执行该请求的 Service Worker。注意该方法仅 Chromium 有值:在 Firefox / WebKit 上调用是安全的,但始终返回null。isNavigationRequest()表示该请求是否驱动 frame 导航;服务端通过_documentId是否存在来判定(Request.isNavigationRequest()),这与服务端按 document 维度跟踪导航的设计一致。
五、请求体:postData、postDataBuffer、postDataJSON
postData()—— 请求体字符串形式,无请求体时为null;postDataBuffer()—— 请求体二进制形式(Buffer),适合非文本负载;postDataJSON()—— 结构化解析请求体。规则是:当Content-Type为application/x-www-form-urlencoded时返回键值对对象;否则按 JSON 解析(JS / Python 返回可序列化对象,C# 返回JsonElement)。
客户端实现印证了这一优先级:postDataJSON()先取postData()文本,命中form-urlencoded时用URLSearchParams逐对解析成对象;否则JSON.parse,解析失败会抛出带原始负载的POST data is not a valid JSON object错误。也就是说它并不是"先看请求头声明",而是先按 Content-Type 分岔、再以 JSON 兜底。
postData()与postDataBuffer()同样会优先返回_fallbackOverrides.postDataBuffer,即被route.continue({ postData })重写后的请求体(客户端 _applyFallbackOverrides 会把字符串、Buffer、可序列化对象三种入参统一转成 Buffer 保存)。
六、请求头:headers()、allHeaders()、headersArray()、headerValue()
四个头相关方法各有分工,核心区别在于是否包含安全类头(如 cookie)以及是否保留头名原始大小写:
| 方法 | 返回 | 特点 |
|---|---|---|
headers() | Object<string, string> | 头名小写化;不含安全相关头(包括 cookie 类) |
allHeaders()(v1.15+) | Object<string, string> | 完整头(含cookie);头名小写化;异步 |
headersArray()(v1.15+) | Array<{ name, value }> | 完整头;头名保留原始大小写;多值头(如Set-Cookie)会出现多条;异步 |
headerValue(name)(v1.15+) | null \| string | 按名称(不区分大小写)取单个头的值;异步 |
实现层面,客户端 headers() 标注了@deprecated,直接读"临时头"_provisionalHeaders;而allHeaders()/headersArray()/headerValue()都经由 _actualHeaders() 走一条带 Promise 缓存的通道,向服务端请求rawRequestHeaders。对应地,服务端 Request 用ManualPromise保存原始头:setRawRequestHeaders()在真正拿到头数据时 resolve,若拿不到则回落到临时头。这也解释了为什么headers()是同步的(数据来自事件携带的快照),而allHeaders()等是异步的(可能要等浏览器补齐真实头)。
RawHeaders 是这一组方法的公共底座:内部用MultiMap按小写名索引;get()对set-cookie这类多值头用换行拼接、其余用", "拼接;headersArray()则原样返回保留大小写与重复项的数组。
七、重定向链:redirectedFrom() 与 redirectedTo()
服务器返回 3xx 时,Playwright 会为新 URL 创建一个新Request,两者通过redirectedFrom()/redirectedTo()互链;多级重定向时可反复调用redirectedFrom()还原整条链路。
文档示例:若http://example.com重定向到https://example.com:
const response = await page.goto('http://example.com'); console.log(response.request().redirectedFrom().url()); // 'http://example.com'Response response = page.navigate("http://example.com"); System.out.println(response.request().redirectedFrom().url()); // "http://example.com"response = await page.goto("http://example.com") print(response.request.redirected_from.url) # "http://example.com"response = page.goto("http://example.com") print(response.request.redirected_from.url) # "http://example.com"var response = await page.GotoAsync("http://www.microsoft.com"); Console.WriteLine(response.Request.RedirectedFrom?.Url); // http://www.microsoft.com若没有发生重定向,redirectedFrom()返回null:
const response = await page.goto('https://google.com'); console.log(response.request().redirectedFrom()); // nullredirectedTo()是其反向操作,文档给出的恒等式在所有语言下都成立:
console.log(request.redirectedFrom().redirectedTo() === request); // trueSystem.out.println(request.redirectedFrom().redirectedTo() == request); // trueassert request.redirected_from.redirected_to == requestConsole.WriteLine(request.RedirectedFrom?.RedirectedTo == request); // True源码中,双向链接在 客户端构造函数 建立:从initializer.redirectedFrom还原出前一个Request后,立即回写this._redirectedFrom._redirectedTo = this;服务端 Request 构造器 中同样有redirectedFrom._redirectedTo = this(L209-L210)。此外两个方向都提供_finalRequest()(客户端 L274-L276、服务端 L303-L305),沿_redirectedTo递归走到链尾,供内部(如路由与导航逻辑)取"最终请求"使用。测试tests/page/page-event-request.spec.ts、tests/page/page-network-request.spec.ts与tests/page/page-goto.spec.ts覆盖了重定向场景下事件序列与redirectedFrom断言,可作为行为验证参考。
八、响应关联:response() 与 existingResponse()
response()(异步)—— 等待并返回匹配的 Response;若因错误未收到响应则返回null。客户端实现 通过 channel 向服务端等待,对应服务端的_waitForResponsePromise,该 Promise 会在_setResponse()或_setFailureText()时 resolve;existingResponse()(since v1.59,同步)—— 只在响应已经到达时立即返回 Response,否则立即返回null,绝不等待。
existingResponse()解决的是事件回调中的时序问题:在request事件的回调里响应尚未到达,response()会挂起等待,而existingResponse()允许"现在就有就取、没有就跳过"的非阻塞模式。实现上它直接读取 客户端_response字段,该字段由 Response 构造函数 在响应对象创建时回填(this._request._response = this)。
九、timing():资源计时
timing()返回 Resource Timing 信息。多数计时值在收到响应后即可用,responseEnd要等请求完成(requestfinished)后才可用。返回对象字段(完整定义见 params.md 的 resource-timing 段落):
startTime:相对 1970-01-01 00:00:00 UTC 的毫秒数(与其他字段不同,它是绝对时间戳);domainLookupStart/domainLookupEnd:DNS 查询前后;connectStart/secureConnectionStart/connectEnd:建连与 TLS 握手(secureConnectionStart仅在安全连接时可用);requestStart/responseStart:发出请求 / 收到首字节;responseEnd:收到最后一字节。
除startTime外的取值均为相对startTime的毫秒数,不可用则为-1。文档多语言示例:
const requestFinishedPromise = page.waitForEvent('requestfinished'); await page.goto('http://example.com'); const request = await requestFinishedPromise; console.log(request.timing());page.onRequestFinished(request -> { Timing timing = request.timing(); System.out.println(timing.responseEnd - timing.startTime); }); page.navigate("http://example.com");async with page.expect_event("requestfinished") as request_info: await page.goto("http://example.com") request = await request_info.value print(request.timing)with page.expect_event("requestfinished") as request_info: page.goto("http://example.com") request = request_info.value print(request.timing)var request = await page.RunAndWaitForRequestFinishedAsync(async () => { await page.GotoAsync("https://www.microsoft.com"); }); Console.WriteLine(request.Timing.ResponseEnd);从源码看,客户端 timing() 只是返回内部_timing对象引用。该对象初始全为-1(构造函数 L114-L124),两处被填充:一是 Response 构造器 在响应到达时Object.assign(this._request._timing, this._initializer.timing),二是 _setResponseEndTiming() 在请求完成时写入responseEnd,并在responseStart缺失时以responseEnd兜底——这正是"多数值随响应用、responseEnd随请求完成"这一文档描述的代码依据,也意味着timing()返回的对象是持续变化的活引用,若在requestfinished前提前拷贝,拿到的可能仍是-1。
十、sizes():请求与响应体积
sizes()(since v1.15,异步)返回四个字节数指标:
requestBodySize—— 请求体大小(POST 负载),无请求体时为 0;requestHeadersSize—— 从 HTTP 请求消息起始到(含)body 前双 CRLF 的总字节数;responseBodySize—— 已收到的响应体(编码后)大小;responseHeadersSize—— 从 HTTP 响应消息起始到(含)body 前双 CRLF 的总字节数。
客户端 sizes() 的实现值得注意:它先await this.response(),若响应为null(请求失败)直接抛出Unable to fetch sizes for failed request,否则转调response._channel.sizes()。也就是说失败请求无法取体积,这是文档未强调但源码明确的约束。请求头大小在服务端由 _requestHeadersSize() 基于原始请求头计算。
十一、综合实战:一个可运行的请求审计脚本
把上述方法组合起来,可以得到一个覆盖"失败审计 + 重定向追踪 + 性能/体积采集"的完整示例(Node.js / JS 语法):
import { chromium } from 'playwright'; const browser = await chromium.launch(); const page = await browser.newPage(); // 1) 审计所有失败请求 page.on('requestfailed', request => { const failure = request.failure(); if (failure) console.log('FAILED', request.url(), failure.errorText); }); // 2) 追踪导航重定向链并采集最终请求数据 const finalRequestPromise = page.waitForEvent('requestfinished', request => request.resourceType() === 'document'); await page.goto('http://example.com', { waitUntil: 'domcontentloaded' }); const finalRequest = await finalRequestPromise; // 还原重定向链 let current = finalRequest; const chain = []; while (current) { chain.push(current.url()); current = current.redirectedFrom(); } console.log('redirect chain:', chain.reverse().join(' -> ')); // 3) 计时与体积 console.log('timing:', finalRequest.timing()); console.log('sizes:', await finalRequest.sizes()); console.log('method:', finalRequest.method()); console.log('resourceType:', finalRequest.resourceType()); // 完整请求头(含 cookie) console.log('all headers keys:', Object.keys(await finalRequest.allHeaders())); await browser.close();对应的测试基建在本仓库中均有落点:事件序列与请求对象行为见tests/page/page-event-request.spec.ts、tests/page/page-network-request.spec.ts,体积采集见tests/page/page-network-sizes.spec.ts,路由改写(route.continue后url()/method()变化)见tests/page/page-request-continue.spec.ts。
十二、与周边类的协作关系小结
- 与 Route:
page.route()的回调接收(route, request),route.continue({ url, method, headers, postData })会写入本文所述的_fallbackOverrides,使url()、method()、postData()、headers()返回改写后的值;路由内部还维护"待处理 handler 链"(见 客户端 RouteHandler),一个 route 可被多个 handler 接力消费; - 与 Response:
request.response()/response.request()双向可取;existingResponse()提供非阻塞查询; - 与 Worker:Service Worker 发起的请求用
request.serviceWorker()定位发起者(仅 Chromium 有值),此时frame()不可用; - 与 HAR / Trace:网络请求事件同样驱动 HAR 录制与 Trace 的网络面板,相关行为可参考
docs/src/network.md与tests/library/har.spec.ts。
参考文件
- 文档骨架:Request 类 API 文档
- 资源计时字段定义:params.md - resource-timing
- 客户端实现:client/network.ts - Request 类、RawHeaders
- 服务端实现:server/network.ts - Request 类
- 行为测试:page-event-request.spec.ts、page-network-request.spec.ts、page-network-sizes.spec.ts、page-request-continue.spec.ts
【免费下载链接】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),仅供参考