Skip to main content

playwright_rs_trace/
network.rs

1//! `trace.network` — HAR-like resource snapshots.
2
3use serde::Deserialize;
4use serde_json::Value;
5
6/// One entry from `trace.network` — a HAR-like resource snapshot
7/// recording a single HTTP request/response pair (or a single redirect
8/// step in a chain).
9#[derive(Debug, Clone)]
10pub struct NetworkEntry {
11    /// `_frameref` — frame GUID. `None` when the trace was recorded
12    /// with `includeTraceInfo: false`.
13    pub frame_ref: Option<String>,
14    /// `pageref` — page GUID.
15    pub page_ref: Option<String>,
16    /// `_monotonicTime` (ms). `None` when `includeTraceInfo` was
17    /// disabled at record time.
18    pub monotonic_time: Option<f64>,
19    /// `startedDateTime` — ISO-8601 wall-clock timestamp of the
20    /// request start.
21    pub started_date_time: String,
22    /// Total request+response time in ms. `None` when timings weren't
23    /// captured (HAR-spec `-1` sentinel mapped to `None` at parse time).
24    pub time: Option<f64>,
25    pub request: RequestSnapshot,
26    pub response: ResponseSnapshot,
27    /// HAR fields we don't model individually (`cookies`, `timings`,
28    /// `cache`, `queryString`, `_transferSize`, …). Preserved verbatim
29    /// for forward-compat and for callers that need them.
30    pub raw_snapshot: Value,
31}
32
33#[derive(Debug, Clone)]
34pub struct RequestSnapshot {
35    pub method: String,
36    pub url: String,
37    pub http_version: String,
38    pub headers: Vec<HeaderEntry>,
39    pub headers_size: Option<u64>,
40    pub body_size: Option<u64>,
41    pub post_data: Option<RequestPostData>,
42}
43
44#[derive(Debug, Clone)]
45pub struct ResponseSnapshot {
46    pub status: Option<u16>,
47    pub status_text: String,
48    pub http_version: String,
49    pub headers: Vec<HeaderEntry>,
50    pub headers_size: Option<u64>,
51    pub body_size: Option<u64>,
52    /// `None` when not a redirect (empty string in the HAR wire).
53    pub redirect_url: Option<String>,
54    pub content: ResponseContent,
55}
56
57#[derive(Debug, Clone)]
58pub struct HeaderEntry {
59    pub name: String,
60    pub value: String,
61}
62
63#[derive(Debug, Clone)]
64pub struct RequestPostData {
65    /// Path of the request body inside the archive, ready to open there.
66    /// Normalized across trace v8 (entry name) and v9 (path).
67    pub file: String,
68}
69
70#[derive(Debug, Clone)]
71pub struct ResponseContent {
72    pub size: Option<u64>,
73    pub mime_type: String,
74    /// Path of the response body inside the archive, ready to open there,
75    /// normalized across trace v8 (entry name) and v9 (path). `None` when
76    /// the response has no body (`204`, `304`, …).
77    pub file: Option<String>,
78}
79
80// ---------------------------------------------------------------------------
81// Wire-format helpers (crate-private)
82// ---------------------------------------------------------------------------
83
84#[derive(Deserialize)]
85#[serde(rename_all = "camelCase")]
86struct SnapshotWire {
87    #[serde(default, rename = "_frameref")]
88    frame_ref: Option<String>,
89    #[serde(default)]
90    pageref: Option<String>,
91    #[serde(default, rename = "_monotonicTime")]
92    monotonic_time: Option<f64>,
93    #[serde(default)]
94    started_date_time: String,
95    #[serde(default = "default_time")]
96    time: f64,
97    request: RequestWire,
98    response: ResponseWire,
99}
100
101fn default_time() -> f64 {
102    -1.0
103}
104
105#[derive(Deserialize)]
106#[serde(rename_all = "camelCase")]
107struct RequestWire {
108    method: String,
109    url: String,
110    #[serde(default)]
111    http_version: String,
112    #[serde(default)]
113    headers: Vec<HeaderEntryWire>,
114    #[serde(default = "default_neg_one")]
115    headers_size: i64,
116    #[serde(default = "default_neg_one")]
117    body_size: i64,
118    #[serde(default)]
119    post_data: Option<PostDataWire>,
120}
121
122#[derive(Deserialize)]
123#[serde(rename_all = "camelCase")]
124struct ResponseWire {
125    #[serde(default = "default_neg_one_i32")]
126    status: i32,
127    #[serde(default)]
128    status_text: String,
129    #[serde(default)]
130    http_version: String,
131    #[serde(default)]
132    headers: Vec<HeaderEntryWire>,
133    #[serde(default = "default_neg_one")]
134    headers_size: i64,
135    #[serde(default = "default_neg_one")]
136    body_size: i64,
137    #[serde(default)]
138    redirect_url: String,
139    content: ContentWire,
140}
141
142#[derive(Deserialize)]
143struct HeaderEntryWire {
144    name: String,
145    value: String,
146}
147
148#[derive(Deserialize)]
149struct PostDataWire {
150    // Trace v8 wrote `_sha1` (an entry name), v9 writes `_file` (a path).
151    #[serde(
152        rename = "_file",
153        alias = "_sha1",
154        deserialize_with = "crate::event::required_resource_path"
155    )]
156    file: String,
157}
158
159#[derive(Deserialize)]
160#[serde(rename_all = "camelCase")]
161struct ContentWire {
162    #[serde(default = "default_neg_one")]
163    size: i64,
164    #[serde(default)]
165    mime_type: String,
166    #[serde(
167        default,
168        rename = "_file",
169        alias = "_sha1",
170        deserialize_with = "crate::event::resource_path"
171    )]
172    file: Option<String>,
173}
174
175fn default_neg_one() -> i64 {
176    -1
177}
178fn default_neg_one_i32() -> i32 {
179    -1
180}
181
182// HAR encodes "unknown" as `-1` for sizes / status / time and as the
183// empty string for `redirectURL`. Public types map both to `None`.
184fn unknown_neg_one_u64(n: i64) -> Option<u64> {
185    if n == -1 { None } else { Some(n as u64) }
186}
187
188fn unknown_neg_one_f64(n: f64) -> Option<f64> {
189    if n == -1.0 { None } else { Some(n) }
190}
191
192fn empty_string_to_none(s: String) -> Option<String> {
193    if s.is_empty() { None } else { Some(s) }
194}
195
196impl NetworkEntry {
197    pub(crate) fn from_snapshot(snapshot: Value) -> Result<Self, serde_json::Error> {
198        // Borrowing deserialization: the wire struct copies only the strings
199        // it keeps, and the tree moves into `raw_snapshot` unchanged.
200        let wire = SnapshotWire::deserialize(&snapshot)?;
201        Ok(NetworkEntry {
202            frame_ref: wire.frame_ref,
203            page_ref: wire.pageref,
204            monotonic_time: wire.monotonic_time,
205            started_date_time: wire.started_date_time,
206            time: unknown_neg_one_f64(wire.time),
207            request: RequestSnapshot {
208                method: wire.request.method,
209                url: wire.request.url,
210                http_version: wire.request.http_version,
211                headers: wire
212                    .request
213                    .headers
214                    .into_iter()
215                    .map(|h| HeaderEntry {
216                        name: h.name,
217                        value: h.value,
218                    })
219                    .collect(),
220                headers_size: unknown_neg_one_u64(wire.request.headers_size),
221                body_size: unknown_neg_one_u64(wire.request.body_size),
222                post_data: wire
223                    .request
224                    .post_data
225                    .map(|p| RequestPostData { file: p.file }),
226            },
227            response: ResponseSnapshot {
228                status: if wire.response.status == -1 {
229                    None
230                } else {
231                    Some(wire.response.status as u16)
232                },
233                status_text: wire.response.status_text,
234                http_version: wire.response.http_version,
235                headers: wire
236                    .response
237                    .headers
238                    .into_iter()
239                    .map(|h| HeaderEntry {
240                        name: h.name,
241                        value: h.value,
242                    })
243                    .collect(),
244                headers_size: unknown_neg_one_u64(wire.response.headers_size),
245                body_size: unknown_neg_one_u64(wire.response.body_size),
246                redirect_url: empty_string_to_none(wire.response.redirect_url),
247                content: ResponseContent {
248                    size: unknown_neg_one_u64(wire.response.content.size),
249                    mime_type: wire.response.content.mime_type,
250                    file: wire.response.content.file,
251                },
252            },
253            raw_snapshot: snapshot,
254        })
255    }
256}