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    /// Points to `resources/<sha1>` in the zip.
66    pub sha1: String,
67}
68
69#[derive(Debug, Clone)]
70pub struct ResponseContent {
71    pub size: Option<u64>,
72    pub mime_type: String,
73    /// Points to `resources/<sha1>`. `None` when the response has no
74    /// body (`204`, `304`, …).
75    pub sha1: Option<String>,
76}
77
78// ---------------------------------------------------------------------------
79// Wire-format helpers (crate-private)
80// ---------------------------------------------------------------------------
81
82#[derive(Deserialize)]
83#[serde(rename_all = "camelCase")]
84struct SnapshotWire {
85    #[serde(default, rename = "_frameref")]
86    frame_ref: Option<String>,
87    #[serde(default)]
88    pageref: Option<String>,
89    #[serde(default, rename = "_monotonicTime")]
90    monotonic_time: Option<f64>,
91    #[serde(default)]
92    started_date_time: String,
93    #[serde(default = "default_time")]
94    time: f64,
95    request: RequestWire,
96    response: ResponseWire,
97}
98
99fn default_time() -> f64 {
100    -1.0
101}
102
103#[derive(Deserialize)]
104#[serde(rename_all = "camelCase")]
105struct RequestWire {
106    method: String,
107    url: String,
108    #[serde(default)]
109    http_version: String,
110    #[serde(default)]
111    headers: Vec<HeaderEntryWire>,
112    #[serde(default = "default_neg_one")]
113    headers_size: i64,
114    #[serde(default = "default_neg_one")]
115    body_size: i64,
116    #[serde(default)]
117    post_data: Option<PostDataWire>,
118}
119
120#[derive(Deserialize)]
121#[serde(rename_all = "camelCase")]
122struct ResponseWire {
123    #[serde(default = "default_neg_one_i32")]
124    status: i32,
125    #[serde(default)]
126    status_text: String,
127    #[serde(default)]
128    http_version: String,
129    #[serde(default)]
130    headers: Vec<HeaderEntryWire>,
131    #[serde(default = "default_neg_one")]
132    headers_size: i64,
133    #[serde(default = "default_neg_one")]
134    body_size: i64,
135    #[serde(default)]
136    redirect_url: String,
137    content: ContentWire,
138}
139
140#[derive(Deserialize)]
141struct HeaderEntryWire {
142    name: String,
143    value: String,
144}
145
146#[derive(Deserialize)]
147struct PostDataWire {
148    #[serde(rename = "_sha1")]
149    sha1: String,
150}
151
152#[derive(Deserialize)]
153#[serde(rename_all = "camelCase")]
154struct ContentWire {
155    #[serde(default = "default_neg_one")]
156    size: i64,
157    #[serde(default)]
158    mime_type: String,
159    #[serde(default, rename = "_sha1")]
160    sha1: Option<String>,
161}
162
163fn default_neg_one() -> i64 {
164    -1
165}
166fn default_neg_one_i32() -> i32 {
167    -1
168}
169
170// HAR encodes "unknown" as `-1` for sizes / status / time and as the
171// empty string for `redirectURL`. Public types map both to `None`.
172fn unknown_neg_one_u64(n: i64) -> Option<u64> {
173    if n == -1 { None } else { Some(n as u64) }
174}
175
176fn unknown_neg_one_f64(n: f64) -> Option<f64> {
177    if n == -1.0 { None } else { Some(n) }
178}
179
180fn empty_string_to_none(s: String) -> Option<String> {
181    if s.is_empty() { None } else { Some(s) }
182}
183
184impl NetworkEntry {
185    pub(crate) fn from_snapshot(snapshot: Value) -> Result<Self, serde_json::Error> {
186        let wire: SnapshotWire = serde_json::from_value(snapshot.clone())?;
187        Ok(NetworkEntry {
188            frame_ref: wire.frame_ref,
189            page_ref: wire.pageref,
190            monotonic_time: wire.monotonic_time,
191            started_date_time: wire.started_date_time,
192            time: unknown_neg_one_f64(wire.time),
193            request: RequestSnapshot {
194                method: wire.request.method,
195                url: wire.request.url,
196                http_version: wire.request.http_version,
197                headers: wire
198                    .request
199                    .headers
200                    .into_iter()
201                    .map(|h| HeaderEntry {
202                        name: h.name,
203                        value: h.value,
204                    })
205                    .collect(),
206                headers_size: unknown_neg_one_u64(wire.request.headers_size),
207                body_size: unknown_neg_one_u64(wire.request.body_size),
208                post_data: wire
209                    .request
210                    .post_data
211                    .map(|p| RequestPostData { sha1: p.sha1 }),
212            },
213            response: ResponseSnapshot {
214                status: if wire.response.status == -1 {
215                    None
216                } else {
217                    Some(wire.response.status as u16)
218                },
219                status_text: wire.response.status_text,
220                http_version: wire.response.http_version,
221                headers: wire
222                    .response
223                    .headers
224                    .into_iter()
225                    .map(|h| HeaderEntry {
226                        name: h.name,
227                        value: h.value,
228                    })
229                    .collect(),
230                headers_size: unknown_neg_one_u64(wire.response.headers_size),
231                body_size: unknown_neg_one_u64(wire.response.body_size),
232                redirect_url: empty_string_to_none(wire.response.redirect_url),
233                content: ResponseContent {
234                    size: unknown_neg_one_u64(wire.response.content.size),
235                    mime_type: wire.response.content.mime_type,
236                    sha1: wire.response.content.sha1,
237                },
238            },
239            raw_snapshot: snapshot,
240        })
241    }
242}