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