ferrijs_fetch/model.rs
1//! The WHATWG request/response model the engine sends and returns.
2//!
3//! [`Request`] is fully resolved before it reaches the engine (absolute
4//! URL, params appended, headers assembled, body materialized): the
5//! engine is format-agnostic. [`Response`] carries the metadata plus a
6//! single-use [`Body`].
7
8use std::time::Duration;
9
10use super::body::Body;
11use super::headers::Headers;
12use super::net_guard::NetGuard;
13
14/// How a redirect response is handled, mirroring WHATWG Fetch's
15/// `RequestInit.redirect` (`follow` | `manual` | `error`).
16#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
17pub enum RedirectMode {
18 /// Follow up to the redirect cap ([`Request::max_redirects`]), then
19 /// error (the default).
20 #[default]
21 Follow,
22 /// Do not follow: the 3xx response is returned as-is (the JS layer
23 /// turns this into an opaque-redirect `Response`).
24 Manual,
25 /// Treat any redirect as a network error.
26 Error,
27}
28
29/// WHATWG `RequestCredentials` — whether stored cookies ride the request.
30#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
31pub enum Credentials {
32 /// Never send credentials (cookies).
33 Omit,
34 /// Send credentials (the default for this engine, and what Playwright's
35 /// `request` always does — the browser context is the jar).
36 #[default]
37 SameOrigin,
38 /// Send credentials on cross-origin requests too. Same behaviour as
39 /// `SameOrigin` here since the engine has a single origin scope.
40 Include,
41}
42
43/// WHATWG `Response.type`: how the response was filtered.
44#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
45pub enum ResponseType {
46 /// A same-origin (unfiltered) response.
47 #[default]
48 Basic,
49 /// A CORS-filtered response.
50 Cors,
51 /// An opaque cross-origin `no-cors` response.
52 Opaque,
53 /// An opaque `redirect: manual` 3xx.
54 OpaqueRedirect,
55 /// A network-error response (`Response.error()`).
56 Error,
57 /// A constructed response with no filtering applied.
58 Default,
59}
60
61impl ResponseType {
62 #[must_use]
63 pub fn as_str(self) -> &'static str {
64 match self {
65 Self::Basic => "basic",
66 Self::Cors => "cors",
67 Self::Opaque => "opaque",
68 Self::OpaqueRedirect => "opaqueredirect",
69 Self::Error => "error",
70 Self::Default => "default",
71 }
72 }
73}
74
75/// Resolved peer address of a response. Mirrors Playwright's
76/// `RemoteAddr` (`{ ipAddress, port }`) returned by
77/// `apiResponse.serverAddr()` / `response.serverAddr()`.
78#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
79pub struct RemoteAddr {
80 #[serde(rename = "ipAddress")]
81 pub ip_address: String,
82 pub port: u16,
83}
84
85/// A fully-resolved request the engine can send as-is.
86#[derive(Debug)]
87pub struct Request {
88 pub method: reqwest::Method,
89 /// Absolute URL with query params already appended.
90 pub url: reqwest::Url,
91 /// Assembled request headers (content-type already set for bodies).
92 pub headers: Headers,
93 pub body: Body,
94 pub redirect: RedirectMode,
95 pub credentials: Credentials,
96 /// Redirect cap for `redirect: follow`: `Some(0)` = don't follow,
97 /// `Some(n)` = follow up to `n`, `None` = the engine default (20).
98 pub max_redirects: Option<u32>,
99 /// Retry the request on a connection reset up to this many times.
100 pub max_retries: u32,
101 pub timeout: Duration,
102 /// Resolved TLS posture for this request.
103 pub ignore_https_errors: bool,
104 /// Sandbox network policy, enforced on the initial URL, every redirect
105 /// hop, and every resolved address when active.
106 pub net_guard: Option<NetGuard>,
107}
108
109/// The engine's response: metadata plus a single-use body.
110#[derive(Debug)]
111pub struct Response {
112 pub status: u16,
113 pub status_text: String,
114 /// Final URL after any followed redirects.
115 pub url: String,
116 pub headers: Headers,
117 pub body: Body,
118 /// Whether at least one redirect hop was followed.
119 pub redirected: bool,
120 /// Whether a 3xx was returned unfollowed because `redirect: manual`.
121 pub unfollowed_redirect: bool,
122 pub server_addr: Option<RemoteAddr>,
123 pub type_: ResponseType,
124}
125
126impl Response {
127 #[must_use]
128 pub fn ok(&self) -> bool {
129 (200..300).contains(&self.status)
130 }
131}