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