unitycatalog_server/rest/
validation.rs1use std::task::{Context, Poll};
10
11use axum::extract::Request;
12use axum::http::StatusCode;
13use axum::response::{IntoResponse, Response};
14use futures_util::FutureExt;
15use futures_util::future::BoxFuture;
16use tower::{Layer, Service};
17
18const MAX_BODY_SIZE: usize = 1024 * 1024;
20
21pub fn max_results_clamp(value: Option<i32>, max: usize) -> Option<usize> {
40 value.map(|v| (v as usize).min(max))
41}
42
43#[derive(Clone)]
54pub struct ValidationMiddleware<S> {
55 inner: S,
56}
57
58impl<S> Service<Request> for ValidationMiddleware<S>
59where
60 S: Service<Request, Response = Response> + Send + 'static,
61 S::Future: Send + 'static,
62{
63 type Response = S::Response;
64 type Error = S::Error;
65 type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
66
67 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
68 self.inner.poll_ready(cx)
69 }
70
71 fn call(&mut self, req: Request) -> Self::Future {
72 if let Some(content_length) = req
74 .headers()
75 .get(axum::http::header::CONTENT_LENGTH)
76 .and_then(|v| v.to_str().ok())
77 .and_then(|s| s.parse::<usize>().ok())
78 && content_length > MAX_BODY_SIZE
79 {
80 let response = (
81 StatusCode::PAYLOAD_TOO_LARGE,
82 format!(
83 "Request body too large: {} bytes (limit: {} bytes)",
84 content_length, MAX_BODY_SIZE
85 ),
86 )
87 .into_response();
88 return async { Ok(response) }.boxed();
89 }
90
91 self.inner.call(req).boxed()
92 }
93}
94
95#[derive(Clone, Default)]
101pub struct ValidationLayer;
102
103impl ValidationLayer {
104 pub fn new() -> Self {
106 Self
107 }
108}
109
110impl<S> Layer<S> for ValidationLayer {
111 type Service = ValidationMiddleware<S>;
112
113 fn layer(&self, inner: S) -> Self::Service {
114 ValidationMiddleware { inner }
115 }
116}
117
118#[cfg(test)]
123mod tests {
124 use axum::body::Body;
125 use axum::http::{Request, StatusCode, header};
126 use tower::{ServiceBuilder, ServiceExt};
127
128 use super::*;
129
130 async fn echo(_req: Request<Body>) -> Result<Response<Body>, std::convert::Infallible> {
131 Ok(Response::new(Body::empty()))
132 }
133
134 #[tokio::test]
135 async fn test_allows_small_body() {
136 let mut svc = ServiceBuilder::new()
137 .layer(ValidationLayer::new())
138 .service_fn(echo);
139
140 let req = Request::post("/")
141 .header(header::CONTENT_LENGTH, "100")
142 .body(Body::empty())
143 .unwrap();
144
145 let resp = svc.ready().await.unwrap().call(req).await.unwrap();
146 assert_eq!(resp.status(), StatusCode::OK);
147 }
148
149 #[tokio::test]
150 async fn test_rejects_oversized_body() {
151 let mut svc = ServiceBuilder::new()
152 .layer(ValidationLayer::new())
153 .service_fn(echo);
154
155 let req = Request::post("/")
156 .header(header::CONTENT_LENGTH, (MAX_BODY_SIZE + 1).to_string())
157 .body(Body::empty())
158 .unwrap();
159
160 let resp = svc.ready().await.unwrap().call(req).await.unwrap();
161 assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
162 }
163
164 #[tokio::test]
165 async fn test_allows_missing_content_length() {
166 let mut svc = ServiceBuilder::new()
167 .layer(ValidationLayer::new())
168 .service_fn(echo);
169
170 let req = Request::post("/").body(Body::empty()).unwrap();
171 let resp = svc.ready().await.unwrap().call(req).await.unwrap();
172 assert_eq!(resp.status(), StatusCode::OK);
173 }
174
175 #[test]
176 fn test_max_results_clamp() {
177 assert_eq!(max_results_clamp(None, 200), None);
178 assert_eq!(max_results_clamp(Some(10), 200), Some(10));
179 assert_eq!(max_results_clamp(Some(200), 200), Some(200));
180 assert_eq!(max_results_clamp(Some(500), 200), Some(200));
181 assert_eq!(max_results_clamp(Some(0), 200), Some(0));
182 }
183}