sift_query_sqlx/web/
search.rs1use std::time::Duration;
2
3use sift_query_core::{ParamError, QueryLimits, RawParams};
4
5use super::{PublicError, WebError};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct SearchParams(pub RawParams);
10
11#[derive(Debug, Clone)]
14pub struct SearchParamConfig {
15 pub limits: QueryLimits,
16 pub body_timeout: Duration,
17}
18
19impl Default for SearchParamConfig {
20 fn default() -> Self {
21 Self {
22 limits: QueryLimits::default(),
23 body_timeout: Duration::from_secs(5),
24 }
25 }
26}
27
28impl SearchParams {
29 #[must_use]
30 pub fn raw(&self) -> &RawParams {
31 &self.0
32 }
33
34 pub fn validate_request(
37 method: &str,
38 query: Option<&str>,
39 content_type: Option<&str>,
40 ) -> Result<(), WebError> {
41 match method {
42 "GET" => Ok(()),
43 "POST" => {
44 if query.is_some_and(|query| !query.is_empty()) {
45 return Err(error(400, "conflicting_query_body"));
46 }
47 let mut parts = content_type.unwrap_or_default().split(';');
48 if !parts
49 .next()
50 .unwrap_or_default()
51 .trim()
52 .eq_ignore_ascii_case("application/x-www-form-urlencoded")
53 || parts.any(|part| {
54 let Some((name, value)) = part.trim().split_once('=') else {
55 return true;
56 };
57 !name.trim().eq_ignore_ascii_case("charset")
58 || !value.trim().trim_matches('"').eq_ignore_ascii_case("utf-8")
59 })
60 {
61 return Err(error(415, "unsupported_query_media_type"));
62 }
63 Ok(())
64 }
65 _ => Err(error(405, "query_method_not_allowed")),
66 }
67 }
68
69 pub fn parse_request(
72 method: &str,
73 query: Option<&str>,
74 content_type: Option<&str>,
75 body: &[u8],
76 limits: &QueryLimits,
77 ) -> Result<Self, WebError> {
78 Self::validate_request(method, query, content_type)?;
79 let input = if method == "GET" {
80 query.unwrap_or_default()
81 } else {
82 if body.len() > limits.max_raw_query_bytes {
83 return Err(error(413, "query_body_too_large"));
84 }
85 std::str::from_utf8(body).map_err(|_| WebError::from(ParamError::InvalidEncoding))?
86 };
87 RawParams::parse_with_limits(input, limits)
88 .map(Self)
89 .map_err(WebError::from)
90 }
91}
92
93pub(super) const fn error(status: u16, code: &'static str) -> WebError {
94 WebError(PublicError { status, code })
95}
96
97#[cfg(any(feature = "axum", feature = "actix-web", feature = "poem", test))]
98pub(super) async fn read_body<S, B, E>(
99 stream: S,
100 config: &SearchParamConfig,
101) -> Result<Vec<u8>, WebError>
102where
103 S: futures_core::Stream<Item = Result<B, E>>,
104 B: AsRef<[u8]>,
105{
106 use futures_util::StreamExt;
107 tokio::time::timeout(config.body_timeout, async {
108 futures_util::pin_mut!(stream);
109 let mut bytes = Vec::new();
110 while let Some(chunk) = stream.next().await {
111 let chunk = chunk.map_err(|_| error(400, "invalid_query_body"))?;
112 let chunk = chunk.as_ref();
113 if chunk.len()
114 > config
115 .limits
116 .max_raw_query_bytes
117 .saturating_sub(bytes.len())
118 {
119 return Err(error(413, "query_body_too_large"));
120 }
121 bytes.extend_from_slice(chunk);
122 }
123 Ok(bytes)
124 })
125 .await
126 .map_err(|_| error(408, "query_body_timeout"))?
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[tokio::test]
134 async fn chunked_bodies_are_bounded_and_pending_readers_expire() {
135 let config = SearchParamConfig {
136 limits: QueryLimits {
137 max_raw_query_bytes: 3,
138 ..Default::default()
139 },
140 body_timeout: Duration::from_millis(5),
141 };
142 let chunks =
143 futures_util::stream::iter([Ok::<_, ()>(b"id".as_slice()), Ok(b"=1".as_slice())]);
144 assert_eq!(
145 read_body(chunks, &config).await.unwrap_err().0.code,
146 "query_body_too_large"
147 );
148 let pending = futures_util::stream::pending::<Result<Vec<u8>, ()>>();
149 assert_eq!(
150 read_body(pending, &config).await.unwrap_err().0.code,
151 "query_body_timeout"
152 );
153 }
154}