asknothingx2_util/api/request/
mod.rs

1mod body;
2mod error;
3
4#[cfg(feature = "stream")]
5pub use body::CodecType;
6
7pub use body::RequestBody;
8pub use error::{HeaderError, StreamError};
9use percent_encoding::{percent_decode_str, utf8_percent_encode, AsciiSet, CONTROLS};
10
11use std::str::FromStr;
12
13use http::{header::CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue, Method};
14use reqwest::{Client, Request, RequestBuilder, Response};
15use url::Url;
16
17use super::{
18    content_type::{Application, Text},
19    setup::get_global_client_or_default,
20};
21
22/// Characters that must be percent-encoded in HTTP header values
23/// Based on RFC 7230 field-vchar = VCHAR / obs-text
24const HEADER_ENCODE_SET: &AsciiSet = &CONTROLS
25    .add(b' ')
26    .add(b'"')
27    .add(b'\\')
28    .add(b'\t')
29    .add(b'\r')
30    .add(b'\n');
31
32/// Stricter encoding for header values that should be safe everywhere
33const HEADER_SAFE_ENCODE_SET: &AsciiSet = &HEADER_ENCODE_SET
34    .add(b'(')
35    .add(b')')
36    .add(b'<')
37    .add(b'>')
38    .add(b'@')
39    .add(b',')
40    .add(b';')
41    .add(b':')
42    .add(b'/')
43    .add(b'[')
44    .add(b']')
45    .add(b'?')
46    .add(b'=')
47    .add(b'{')
48    .add(b'}');
49
50/// For RFC 8187 filename* encoding - encode control chars and non-ASCII
51/// RFC 8187 allows most ASCII chars but requires encoding of control chars
52const RFC8187_ENCODE_SET: &AsciiSet = CONTROLS;
53
54/// Alternative: More restrictive set for RFC 8187 if needed
55/// This encodes additional characters that might cause issues in filenames
56const RFC8187_SAFE_ENCODE_SET: &AsciiSet = &CONTROLS
57    .add(b' ')
58    .add(b'"')
59    .add(b'%')
60    .add(b'*')
61    .add(b'/')
62    .add(b'\\')
63    .add(b'?')
64    .add(b'<')
65    .add(b'>')
66    .add(b'|');
67
68pub trait IntoRequestParts {
69    fn into_request_parts(self) -> RequestParts;
70}
71
72#[derive(Debug)]
73pub struct RequestParts {
74    pub method: Method,
75    pub url: Url,
76    pub headers: HeaderMap,
77    pub body: Option<RequestBody>,
78    pub version: Option<http::Version>,
79    pub timeout: Option<std::time::Duration>,
80    pub request_id: Option<String>,
81}
82
83impl RequestParts {
84    pub fn new(method: Method, url: Url) -> Self {
85        Self {
86            method,
87            url,
88            headers: HeaderMap::new(),
89            body: None,
90            version: None,
91            timeout: None,
92            request_id: None,
93        }
94    }
95
96    pub fn header(mut self, key: impl AsRef<str>, value: impl AsRef<str>) -> Self {
97        if let (Ok(name), Ok(val)) = (
98            HeaderName::from_str(key.as_ref()),
99            HeaderValue::from_str(value.as_ref()),
100        ) {
101            self.headers.insert(name, val);
102        }
103        self
104    }
105
106    pub fn try_header(
107        mut self,
108        key: impl AsRef<str>,
109        value: impl AsRef<str>,
110    ) -> Result<Self, HeaderError> {
111        let key_str = key.as_ref();
112        let value_str = value.as_ref();
113
114        let name = HeaderName::from_str(key_str).map_err(|e| HeaderError::InvalidHeaderName {
115            name: key_str.to_string(),
116            reason: e.to_string(),
117        })?;
118
119        let val =
120            HeaderValue::from_str(value_str).map_err(|e| HeaderError::InvalidHeaderValue {
121                name: key_str.to_string(),
122                value: value_str.to_string(),
123                reason: e.to_string(),
124            })?;
125
126        self.headers.insert(name, val);
127        Ok(self)
128    }
129
130    // pub fn header_encoded(mut self, key: impl AsRef<str>, value: impl AsRef<str>) -> Self {
131    //     let key_str = key.as_ref();
132    //     let value_str = value.as_ref();
133    //
134    //     let name = match HeaderName::from_str(key_str) {
135    //         Ok(name) => name,
136    //         Err(e) => {
137    //             warn!(
138    //                 header_name = key_str,
139    //                 error =%e,
140    //                 "Invalid header name"
141    //             );
142    //             return self;
143    //         }
144    //     };
145    //
146    //     if let Ok(val) = HeaderValue::from_str(value_str) {
147    //         self.headers.insert(name, val);
148    //         return self;
149    //     }
150    //
151    //     let encoded_value = utf8_percent_encode(value_str, HEADER_ENCODE_SET).to_string();
152    //     if let Ok(val) = HeaderValue::from_str(&encoded_value) {
153    //         debug!(
154    //             header_name = key_str,
155    //             original_value = value_str,
156    //             encoded_value = %encoded_value,
157    //             "Header value percent-encoded"
158    //         );
159    //         self.headers.insert(name, val);
160    //     } else {
161    //         warn!(
162    //             header_name = key_str,
163    //             header_value = value_str,
164    //             encoded_value = %encoded_value,
165    //             "Could not set header even with percent encoding"
166    //         );
167    //     }
168    //
169    //     self
170    // }
171
172    // pub fn header_rfc8187(mut self, key: impl AsRef<str>, value: impl AsRef<str>) -> Self {
173    //     let key_str = key.as_ref();
174    //     let value_str = value.as_ref();
175    //
176    //     let name = match HeaderName::from_str(key_str) {
177    //         Ok(name) => name,
178    //         Err(e) => {
179    //             warn!(
180    //                 header_name = key_str,
181    //                 error = %e,
182    //                 "Invalid header name for RFC 8187 encoding"
183    //             );
184    //             return self;
185    //         }
186    //     };
187    //
188    //     let encoded_value = if value_str.is_ascii() {
189    //         value_str.to_string()
190    //     } else {
191    //         let percent_encoded = utf8_percent_encode(value_str, RFC8187_ENCODE_SET).to_string();
192    //         let rfc8187_value = format!("utf-8''{percent_encoded}");
193    //         debug!(
194    //             header_name = key_str,
195    //             original_value = value_str,
196    //             rfc8187_value = %rfc8187_value,
197    //             "Applied RFC 8187 encoding to header value"
198    //         );
199    //         rfc8187_value
200    //     };
201    //
202    //     if let Ok(val) = HeaderValue::from_str(&encoded_value) {
203    //         self.headers.insert(name, val);
204    //     } else {
205    //         warn!(
206    //             header_name = key_str,
207    //             original_value = value_str,
208    //             encoded_value = %encoded_value,
209    //             "Could not set header with RFC 8187 encoding"
210    //         );
211    //     }
212    //
213    //     self
214    // }
215
216    // Set Content-Disposition with proper filename encoding
217    //
218    // This is a convenience method that handles both `filename` and `filename*`
219    // parameters according to RFC 6266 for maximum browser compatibility.
220    // pub fn content_disposition_attachment(mut self, filename: impl AsRef<str>) -> Self {
221    //     let filename_str = filename.as_ref();
222    //
223    //     // Create ASCII fallback by replacing non-ASCII chars
224    //     let ascii_fallback = filename_str
225    //         .chars()
226    //         .map(|c| {
227    //             if c.is_ascii() && c != '"' && c != '\\' {
228    //                 c
229    //             } else {
230    //                 '_'
231    //             }
232    //         })
233    //         .collect::<String>();
234    //
235    //     let disposition_value = if filename_str.is_ascii() {
236    //         // Simple case: pure ASCII filename
237    //         debug!(
238    //             filename = filename_str,
239    //             "Setting ASCII filename in Content-Disposition"
240    //         );
241    //
242    //         format!("attachment; filename=\"{filename_str}\"")
243    //     } else {
244    //         // Complex case: provide both filename and filename* for compatibility
245    //         let encoded_filename =
246    //             utf8_percent_encode(filename_str, RFC8187_ENCODE_SET).to_string();
247    //         debug!(
248    //             filename = filename_str,
249    //             ascii_fallback = %ascii_fallback,
250    //             encoded_filename = %encoded_filename,
251    //             "Setting international filename with RFC 8187 encoding"
252    //         );
253    //
254    //         format!(
255    //             "attachment; filename=\"{ascii_fallback}\"; filename*=utf-8''{encoded_filename}",
256    //         )
257    //     };
258    //
259    //     self.header("Content-Disposition", disposition_value)
260    // }
261
262    pub fn headers(mut self, headers: HeaderMap) -> Self {
263        self.headers.extend(headers);
264        self
265    }
266
267    pub fn body(mut self, body: RequestBody) -> Self {
268        self.body = Some(body);
269        self
270    }
271
272    pub fn text(mut self, text: impl Into<String>) -> Self {
273        self.body = Some(RequestBody::from_string(text.into()));
274        self.header(CONTENT_TYPE, Text::Plain)
275    }
276
277    pub fn json(mut self, value: serde_json::Value) -> Self {
278        self.body = Some(RequestBody::from_json(value));
279        self.header(CONTENT_TYPE, Application::Json)
280    }
281
282    pub fn form(mut self, form: Vec<(String, String)>) -> Self {
283        self.body = Some(RequestBody::from_form(form));
284        self.header(CONTENT_TYPE, Application::FormUrlEncoded)
285    }
286
287    pub fn form_pairs<I, K, V>(mut self, pairs: I) -> Self
288    where
289        I: IntoIterator<Item = (K, V)>,
290        K: Into<String>,
291        V: Into<String>,
292    {
293        self.body = Some(RequestBody::from_form_pairs(pairs));
294        self.header(CONTENT_TYPE, Application::FormUrlEncoded)
295    }
296
297    pub fn multipart(mut self, form: reqwest::multipart::Form) -> Self {
298        self.body = Some(RequestBody::from_multipart(form));
299        // Note: reqwest will set the content-type with boundary automatically
300        self
301    }
302
303    pub fn empty(mut self) -> Self {
304        self.body = Some(RequestBody::empty());
305        self
306    }
307
308    pub fn version(mut self, version: http::Version) -> Self {
309        self.version = Some(version);
310        self
311    }
312
313    pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
314        self.timeout = Some(timeout);
315        self
316    }
317
318    pub fn request_id(mut self, request_id: impl Into<String>) -> Self {
319        self.request_id = Some(request_id.into());
320        self
321    }
322
323    pub fn into_request_builder(self, client: &Client) -> (RequestBuilder, Option<String>) {
324        let mut builder = client.request(self.method, self.url);
325
326        if !self.headers.is_empty() {
327            builder = builder.headers(self.headers);
328        }
329
330        if let Some(version) = self.version {
331            builder = builder.version(version);
332        }
333
334        if let Some(timeout) = self.timeout {
335            builder = builder.timeout(timeout);
336        }
337
338        if let Some(body) = self.body {
339            builder = body.into_reqwest_body(builder);
340        }
341
342        (builder, self.request_id)
343    }
344
345    pub fn into_request(self, client: &Client) -> Result<Request, reqwest::Error> {
346        let (request_builder, _) = self.into_request_builder(client);
347        request_builder.build()
348    }
349
350    pub async fn send(self) -> Result<Response, reqwest::Error> {
351        let (request_builder, _) = self.into_request_builder(get_global_client_or_default());
352        request_builder.send().await
353    }
354
355    #[cfg(feature = "stream")]
356    pub fn from_file(mut self, file: tokio::fs::File) -> Self {
357        self.body = Some(RequestBody::from_file(file));
358        self
359    }
360
361    #[cfg(feature = "stream")]
362    pub fn from_file_buffered(mut self, file: tokio::fs::File, buffer_size: usize) -> Self {
363        self.body = Some(RequestBody::from_file_buffered(file, buffer_size));
364        self
365    }
366
367    #[cfg(feature = "stream")]
368    pub async fn from_file_path<P: AsRef<std::path::Path>>(
369        mut self,
370        path: P,
371    ) -> Result<Self, StreamError> {
372        self.body = Some(RequestBody::from_file_path(path).await?);
373        Ok(self)
374    }
375
376    #[cfg(feature = "stream")]
377    pub async fn from_file_path_buffered<P: AsRef<std::path::Path>>(
378        mut self,
379        path: P,
380        buffer_size: usize,
381    ) -> Result<Self, StreamError> {
382        self.body = Some(RequestBody::from_file_path_buffered(path, buffer_size).await?);
383        Ok(self)
384    }
385
386    #[cfg(feature = "stream")]
387    pub fn from_async_read<R>(mut self, reader: R) -> Self
388    where
389        R: tokio::io::AsyncRead + Send + Sync + 'static,
390    {
391        self.body = Some(RequestBody::from_async_read(reader));
392        self
393    }
394
395    #[cfg(feature = "stream")]
396    pub fn from_tcp_stream(mut self, tcp: tokio::net::TcpStream) -> Self {
397        self.body = Some(RequestBody::from_tcp_stream(tcp));
398        self
399    }
400
401    #[cfg(feature = "stream")]
402    pub fn from_command_output(
403        mut self,
404        command: tokio::process::Command,
405    ) -> Result<Self, StreamError> {
406        self.body = Some(RequestBody::from_command_output(command)?);
407        Ok(self)
408    }
409
410    #[cfg(feature = "stream")]
411    pub fn stream<S>(mut self, stream: S) -> Self
412    where
413        S: futures_util::Stream<Item = Result<bytes::Bytes, StreamError>> + Send + Sync + 'static,
414    {
415        self.body = Some(RequestBody::from_stream(stream));
416        self
417    }
418
419    #[cfg(feature = "stream")]
420    pub fn io_stream<S>(mut self, stream: S) -> Self
421    where
422        S: futures_util::Stream<Item = Result<bytes::Bytes, std::io::Error>>
423            + Send
424            + Sync
425            + 'static,
426    {
427        self.body = Some(RequestBody::from_io_stream(stream));
428        self
429    }
430}
431
432impl IntoRequestParts for RequestParts {
433    fn into_request_parts(self) -> RequestParts {
434        self
435    }
436}
437
438/// Decode a percent-encoded header value back to UTF-8
439pub fn decode_header_value(encoded: &str) -> Result<String, HeaderError> {
440    percent_decode_str(encoded)
441        .decode_utf8()
442        .map(|cow| cow.into_owned())
443        .map_err(|e| HeaderError::InvalidUtf8 {
444            reason: e.to_string(),
445        })
446}
447
448/// Encode a string using the safe header encoding set
449pub fn encode_header(value: &str) -> String {
450    utf8_percent_encode(value, HEADER_SAFE_ENCODE_SET).to_string()
451}
452
453/// Create an RFC 8187 encoded value
454pub fn encode_rfc8187(value: &str) -> String {
455    if value.is_ascii() {
456        value.to_string()
457    } else {
458        let encoded = utf8_percent_encode(value, RFC8187_ENCODE_SET).to_string();
459        format!("utf-8''{encoded}")
460    }
461}