cognee_http_server/middleware/
validation.rs1use axum::{
8 body::Bytes,
9 extract::{FromRequest, FromRequestParts, Request},
10 http::{header, request::Parts},
11};
12use serde::de::DeserializeOwned;
13use serde_json::json;
14
15use crate::error::{ApiError, ValidationDetails};
16
17pub struct LoginForm<T>(pub T);
31
32impl<T, S> FromRequest<S> for LoginForm<T>
33where
34 T: DeserializeOwned,
35 S: Send + Sync,
36{
37 type Rejection = ApiError;
38
39 async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
40 match axum::extract::Form::<T>::from_request(req, state).await {
41 Ok(axum::extract::Form(value)) => Ok(LoginForm(value)),
42 Err(_) => Err(ApiError::LoginBadCredentials),
43 }
44 }
45}
46
47pub struct Json<T>(pub T);
51
52impl<T, S> FromRequest<S> for Json<T>
53where
54 T: DeserializeOwned,
55 S: Send + Sync,
56{
57 type Rejection = ApiError;
58
59 async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
60 let content_type = req
62 .headers()
63 .get(header::CONTENT_TYPE)
64 .and_then(|v| v.to_str().ok())
65 .unwrap_or_default()
66 .to_lowercase();
67
68 if !content_type.contains("application/json") {
69 return Err(ApiError::Validation(ValidationDetails {
70 detail: json!([{
71 "loc": ["headers", "content-type"],
72 "msg": "content-type must be application/json",
73 "type": "value_error"
74 }]),
75 body: None,
76 }));
77 }
78
79 let bytes = Bytes::from_request(req, state).await.map_err(|e| {
81 ApiError::Validation(ValidationDetails {
82 detail: json!([{"loc": ["body"], "msg": e.to_string(), "type": "read_error"}]),
83 body: None,
84 })
85 })?;
86
87 match serde_json::from_slice::<T>(&bytes) {
89 Ok(value) => Ok(Json(value)),
90 Err(err) => {
91 let raw_body = serde_json::from_slice::<serde_json::Value>(&bytes).ok();
93 Err(ApiError::Validation(ValidationDetails {
94 detail: json!([{
95 "loc": ["body"],
96 "msg": err.to_string(),
97 "type": "value_error.json_parse"
98 }]),
99 body: raw_body,
100 }))
101 }
102 }
103 }
104}
105
106pub struct Query<T>(pub T);
125
126impl<T, S> FromRequestParts<S> for Query<T>
127where
128 T: DeserializeOwned,
129 S: Send + Sync,
130{
131 type Rejection = ApiError;
132
133 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
134 let raw = parts.uri.query().unwrap_or("");
135 let de = serde_urlencoded::Deserializer::new(form_urlencoded::parse(raw.as_bytes()));
140 match serde_path_to_error::deserialize::<_, T>(de) {
141 Ok(value) => Ok(Query(value)),
142 Err(err) => {
143 let path = err.path().to_string();
144 let inner_msg = err.into_inner().to_string();
145 let loc = if path.is_empty() || path == "." {
146 json!(["query"])
147 } else {
148 let leaf = path.rsplit('.').next().unwrap_or(path.as_str());
151 json!(["query", leaf])
152 };
153 Err(ApiError::Validation(ValidationDetails {
154 detail: json!([{
155 "loc": loc,
156 "msg": inner_msg,
157 "type": "value_error"
158 }]),
159 body: None,
160 }))
161 }
162 }
163 }
164}
165
166pub use Query as ValidatedQuery;
171
172#[cfg(test)]
175#[allow(
176 clippy::unwrap_used,
177 clippy::expect_used,
178 reason = "test code — panics are acceptable failures"
179)]
180mod tests {
181 use super::*;
182 use axum::{
183 Router,
184 body::{Body, to_bytes},
185 http::{Request, StatusCode},
186 routing::{get, post},
187 };
188 use serde::Deserialize;
189 use tower::ServiceExt;
190
191 #[derive(Deserialize)]
192 struct Payload {
193 name: String,
194 }
195
196 async fn handler(Json(p): Json<Payload>) -> String {
197 p.name
198 }
199
200 fn app() -> Router {
201 Router::new().route("/", post(handler))
202 }
203
204 #[tokio::test]
205 async fn test_missing_required_field_yields_validation_error() {
206 let req = Request::builder()
207 .method("POST")
208 .uri("/")
209 .header("content-type", "application/json")
210 .body(Body::from(r#"{"other": "value"}"#))
211 .expect("request");
212
213 let resp = app().oneshot(req).await.expect("response");
214 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
215
216 let bytes = to_bytes(resp.into_body(), usize::MAX).await.expect("bytes");
217 let body: serde_json::Value = serde_json::from_slice(&bytes).expect("json");
218
219 assert!(body["detail"].is_array(), "detail should be array: {body}");
221 }
222
223 #[tokio::test]
224 async fn test_wrong_content_type_yields_validation_error() {
225 let req = Request::builder()
226 .method("POST")
227 .uri("/")
228 .header("content-type", "text/plain")
229 .body(Body::from(r#"{"name": "test"}"#))
230 .expect("request");
231
232 let resp = app().oneshot(req).await.expect("response");
233 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
234 }
235
236 #[tokio::test]
237 async fn test_valid_json_succeeds() {
238 let req = Request::builder()
239 .method("POST")
240 .uri("/")
241 .header("content-type", "application/json")
242 .body(Body::from(r#"{"name": "alice"}"#))
243 .expect("request");
244
245 let resp = app().oneshot(req).await.expect("response");
246 assert_eq!(resp.status(), StatusCode::OK);
247 }
248
249 #[derive(Deserialize, Default)]
252 enum TestOrderBy {
253 #[default]
254 #[serde(rename = "last_activity_at")]
255 LastActivityAt,
256 #[serde(rename = "started_at")]
257 StartedAt,
258 }
259
260 #[derive(Deserialize)]
261 struct TestQuery {
262 #[serde(default)]
263 order_by: TestOrderBy,
264 #[serde(default = "default_limit")]
265 limit: u32,
266 }
267
268 fn default_limit() -> u32 {
269 50
270 }
271
272 async fn query_handler(Query(q): Query<TestQuery>) -> String {
273 format!(
274 "limit={} ord={}",
275 q.limit,
276 matches!(q.order_by, TestOrderBy::LastActivityAt)
277 )
278 }
279
280 fn query_app() -> Router {
281 Router::new().route("/", get(query_handler))
282 }
283
284 #[tokio::test]
285 async fn valid_query_succeeds() {
286 let req = Request::builder()
287 .method("GET")
288 .uri("/?order_by=started_at&limit=42")
289 .body(Body::empty())
290 .expect("request");
291
292 let resp = query_app().oneshot(req).await.expect("response");
293 assert_eq!(resp.status(), StatusCode::OK);
294 }
295
296 #[tokio::test]
297 async fn unknown_order_by_returns_400_with_python_envelope() {
298 let req = Request::builder()
299 .method("GET")
300 .uri("/?order_by=banana")
301 .body(Body::empty())
302 .expect("request");
303
304 let resp = query_app().oneshot(req).await.expect("response");
305 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
306 let bytes = to_bytes(resp.into_body(), usize::MAX).await.expect("bytes");
307 let body: serde_json::Value = serde_json::from_slice(&bytes).expect("json");
308
309 assert!(body["detail"].is_array(), "detail should be array: {body}");
310 let entry = &body["detail"][0];
311 let loc = entry["loc"].as_array().expect("loc array");
312 assert_eq!(loc[0], "query");
313 assert_eq!(loc[1], "order_by", "loc should target order_by: {body}");
315 let ty = entry["type"].as_str().expect("type str");
316 assert!(
317 ty.ends_with("value_error"),
318 "type should be value_error: {ty}"
319 );
320 }
321
322 #[tokio::test]
323 async fn out_of_range_limit_returns_400_with_python_envelope() {
324 let req = Request::builder()
327 .method("GET")
328 .uri("/?limit=-1")
329 .body(Body::empty())
330 .expect("request");
331
332 let resp = query_app().oneshot(req).await.expect("response");
333 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
334 let bytes = to_bytes(resp.into_body(), usize::MAX).await.expect("bytes");
335 let body: serde_json::Value = serde_json::from_slice(&bytes).expect("json");
336
337 assert!(body["detail"].is_array());
338 let entry = &body["detail"][0];
339 let loc = entry["loc"].as_array().expect("loc array");
340 assert_eq!(loc[0], "query");
341 assert_eq!(loc[1], "limit", "loc should target limit: {body}");
342 let ty = entry["type"].as_str().expect("type str");
343 assert!(ty.ends_with("value_error"));
344 }
345}