1#![forbid(unsafe_code)]
2mod compress;
3mod debug;
4mod encode;
5mod send;
6
7pub use compress::ContentEncoding;
8
9use http::{HeaderName, HeaderValue, Method};
10
11use crate::profile::{HeaderAnchor, Preset};
12
13use crate::core::body::Body;
14use crate::core::config::{ProxyConfig, RedirectPolicy, TimeoutConfig};
15use crate::core::digest::DigestAuth;
16use crate::core::error::{Error, Kind};
17use crate::core::headers::HeaderList;
18use crate::core::into_url::IntoUrl;
19#[cfg(feature = "multipart")]
20use crate::core::multipart::Form;
21use crate::core::retry::RetryPolicy;
22use crate::core::session::Session;
23
24pub trait IntoParamPair {
25 fn into_param_pair(self) -> (String, String);
26}
27
28impl<K, V> IntoParamPair for (K, V)
29where
30 K: AsRef<str>,
31 V: AsRef<str>,
32{
33 fn into_param_pair(self) -> (String, String) {
34 (self.0.as_ref().to_string(), self.1.as_ref().to_string())
35 }
36}
37
38impl<K, V> IntoParamPair for &(K, V)
39where
40 K: AsRef<str>,
41 V: AsRef<str>,
42{
43 fn into_param_pair(self) -> (String, String) {
44 (self.0.as_ref().to_string(), self.1.as_ref().to_string())
45 }
46}
47
48#[must_use = "builders are lazy: nothing happens until `.send()` / `.build()`"]
49pub struct RequestBuilder {
50 pub(super) session: Session,
51 pub(super) method: Method,
52 pub(super) url: String,
53 pub(super) preset: Option<Preset>,
54 pub(super) body: Body,
55 pub(super) headers: HeaderList,
56 pub(super) query_params: Vec<(String, String)>,
57 pub(super) timeouts: Option<TimeoutConfig>,
58 pub(super) builder_error: Option<Error>,
59 pub(super) stream_response: bool,
60 pub(super) compress: Option<ContentEncoding>,
61 pub(super) retry_policy: RetryPolicy,
62 pub(super) digest_auth: Option<DigestAuth>,
63 pub(super) proxy: Option<ProxyConfig>,
64 pub(super) header_order: Option<Vec<String>>,
65 pub(super) preset_user: bool,
66 pub(super) redirect: Option<RedirectPolicy>,
67}
68
69fn default_preset(session: &Session, method: &Method) -> Option<Preset> {
70 if !session.impersonates() {
71 return None;
72 }
73 match *method {
74 Method::GET | Method::HEAD => Some(Preset::Navigate),
75 _ => None,
76 }
77}
78
79impl RequestBuilder {
80 pub(crate) fn new(session: &Session, method: Method, url: &str) -> Self {
81 let preset = default_preset(session, &method);
82 Self {
83 session: session.clone(),
84 method,
85 url: url.to_string(),
86 preset,
87 body: Body::default(),
88 headers: HeaderList::new(),
89 query_params: Vec::new(),
90 timeouts: None,
91 builder_error: None,
92 stream_response: false,
93 compress: None,
94 retry_policy: session.default_retry().clone(),
95 digest_auth: None,
96 proxy: None,
97 header_order: None,
98 preset_user: false,
99 redirect: None,
100 }
101 }
102
103 pub(crate) fn from_url(session: &Session, method: Method, url: impl IntoUrl) -> Self {
104 match url.into_url() {
105 Ok(url) => Self::new(session, method, url.as_str()),
106 Err(err) => {
107 let mut builder = Self::new(session, method, "");
108 builder.builder_error = Some(err);
109 builder
110 }
111 }
112 }
113
114 fn fail(&mut self, err: Error) {
115 if self.builder_error.is_none() {
116 self.builder_error = Some(err);
117 }
118 }
119
120 fn infer_from_content_type(&mut self) {
121 if self.preset_user || !self.session.impersonates() {
122 return;
123 }
124 if !matches!(self.method, Method::POST | Method::PUT | Method::PATCH) {
125 return;
126 }
127 let Some(ct) = self.headers.get("content-type") else {
128 return;
129 };
130 let ct = String::from_utf8_lossy(ct.as_bytes()).to_ascii_lowercase();
131 if ct.starts_with("application/json") {
132 self.preset = Some(Preset::Xhr);
133 } else if ct.starts_with("application/x-www-form-urlencoded") {
134 self.preset = Some(Preset::Form);
135 }
136 }
137
138 pub fn timeout(mut self, config: impl Into<TimeoutConfig>) -> Self {
139 self.timeouts = Some(config.into());
140 self
141 }
142
143 pub fn preset(mut self, preset: Preset) -> Self {
144 self.preset = Some(preset);
145 self.preset_user = true;
146 self
147 }
148
149 pub fn header_order(mut self, order: &[&str]) -> Self {
150 self.header_order = Some(order.iter().map(|s| (*s).to_string()).collect());
151 self
152 }
153
154 pub fn body(mut self, body: impl Into<Body>) -> Self {
155 self.body = body.into();
156 self
157 }
158
159 pub fn json(mut self, value: &impl serde::Serialize) -> Self {
160 match serde_json::to_vec(value) {
161 Ok(bytes) => {
162 self.put("content-type", "application/json");
163 self.body = Body::from(bytes);
164 }
165 Err(e) => {
166 self.builder_error = Some(Error::new(Kind::Json).with_source(e));
167 }
168 }
169 self
170 }
171
172 pub fn form<I, P>(mut self, params: I) -> Self
173 where
174 I: IntoIterator<Item = P>,
175 P: IntoParamPair,
176 {
177 let pairs = collect_pairs(params);
178 let encoded = encode::url_encode_pairs(&pairs);
179 self.put("content-type", "application/x-www-form-urlencoded");
180 self.body = Body::from(encoded.into_bytes());
181 self
182 }
183
184 pub fn stream(mut self) -> Self {
185 self.stream_response = true;
186 self
187 }
188
189 pub fn compress(mut self, encoding: ContentEncoding) -> Self {
190 self.compress = Some(encoding);
191 self
192 }
193
194 pub fn query<I, P>(mut self, params: I) -> Self
195 where
196 I: IntoIterator<Item = P>,
197 P: IntoParamPair,
198 {
199 for pair in params {
200 self.query_params.push(pair.into_param_pair());
201 }
202 self
203 }
204
205 pub fn header(
206 mut self,
207 name: impl TryInto<HeaderName>,
208 value: impl TryInto<HeaderValue>,
209 ) -> Self {
210 if let Err(err) = self.headers.append(name, value) {
211 self.fail(err);
212 }
213 self
214 }
215
216 fn put(&mut self, name: impl TryInto<HeaderName>, value: impl TryInto<HeaderValue>) {
217 if let Err(err) = self.headers.set(name, value) {
218 self.fail(err);
219 }
220 }
221
222 pub fn headers<I, P>(mut self, headers: I) -> Self
223 where
224 I: IntoIterator<Item = P>,
225 P: IntoParamPair,
226 {
227 for pair in headers {
228 let (k, v) = pair.into_param_pair();
229 if let Err(err) = self.headers.append(k, v) {
230 self.fail(err);
231 return self;
232 }
233 }
234 self
235 }
236
237 pub fn anchored(
238 mut self,
239 anchor: HeaderAnchor,
240 name: impl TryInto<HeaderName>,
241 value: impl TryInto<HeaderValue>,
242 ) -> Self {
243 if let Err(err) = self.headers.append_anchored(anchor, name, value) {
244 self.fail(err);
245 }
246 self
247 }
248
249 pub fn bearer_auth(mut self, token: &str) -> Self {
250 self.put("authorization", format!("Bearer {token}"));
251 self
252 }
253
254 pub fn basic_auth(mut self, username: &str, password: &str) -> Self {
255 let encoded = crate::util::base64_encode(&format!("{username}:{password}"));
256 self.put("authorization", format!("Basic {encoded}"));
257 self
258 }
259
260 pub fn retry(mut self, policy: RetryPolicy) -> Self {
261 self.retry_policy = policy;
262 self
263 }
264
265 pub fn redirect(mut self, policy: RedirectPolicy) -> Self {
266 self.redirect = Some(policy);
267 self
268 }
269
270 pub fn digest_auth(mut self, auth: DigestAuth) -> Self {
271 self.digest_auth = Some(auth);
272 self
273 }
274
275 #[cfg(feature = "multipart")]
276 pub fn multipart(mut self, form: Form) -> Self {
277 self.put("content-type", form.content_type());
278 self.body = form.into_stream_body();
279 self
280 }
281
282 pub fn proxy(mut self, config: impl Into<ProxyConfig>) -> Self {
283 self.proxy = Some(config.into());
284 self
285 }
286}
287
288#[cfg(test)]
289#[path = "mod_tests.rs"]
290mod tests;
291
292fn collect_pairs<I, P>(params: I) -> Vec<(String, String)>
293where
294 I: IntoIterator<Item = P>,
295 P: IntoParamPair,
296{
297 params
298 .into_iter()
299 .map(IntoParamPair::into_param_pair)
300 .collect()
301}