rama_http/layer/uri/
data.rs1use rama_core::{
11 Layer, Service,
12 error::{BoxError, BoxErrorExt, ErrorContext},
13};
14use rama_net::{Protocol, uri::DataUri};
15use rama_utils::macros::define_inner_service_accessors;
16
17use crate::{
18 Body, Method, Request, Response, StatusCode,
19 headers::{ContentType, HttpResponseBuilderExt as _},
20};
21
22#[derive(Debug, Clone, Default)]
26#[non_exhaustive]
27pub struct DataUriLayer;
28
29impl DataUriLayer {
30 #[must_use]
32 pub const fn new() -> Self {
33 Self
34 }
35}
36
37impl<S> Layer<S> for DataUriLayer {
38 type Service = DataUriService<S>;
39
40 fn layer(&self, inner: S) -> Self::Service {
41 DataUriService::new(inner)
42 }
43}
44
45#[derive(Debug, Clone)]
49pub struct DataUriService<S> {
50 inner: S,
51}
52
53impl<S> DataUriService<S> {
54 pub const fn new(inner: S) -> Self {
56 Self { inner }
57 }
58
59 define_inner_service_accessors!();
60}
61
62impl<S, ReqBody> Service<Request<ReqBody>> for DataUriService<S>
63where
64 S: Service<Request<ReqBody>, Output = Response, Error: Into<BoxError>>,
65 ReqBody: Send + 'static,
66{
67 type Output = Response;
68 type Error = BoxError;
69
70 async fn serve(&self, req: Request<ReqBody>) -> Result<Self::Output, Self::Error> {
71 if req.uri().scheme() != Some(&Protocol::DATA) {
72 return self.inner.serve(req).await.map_err(Into::into);
73 }
74
75 if !matches!(req.method(), &Method::GET | &Method::HEAD) {
76 return Err(BoxError::from_static_str(
77 "data: URIs support GET and HEAD only",
78 ));
79 }
80
81 let data = DataUri::try_from_uri(req.uri()).context("decode data: uri")?;
82 let media_type = data.media_type().clone();
83
84 let body = if req.method() == Method::HEAD {
85 Body::empty()
86 } else {
87 Body::from(data.into_data())
88 };
89 Response::builder()
90 .status(StatusCode::OK)
91 .typed_header(ContentType::new(media_type))
92 .body(body)
93 .context("build data: response")
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use std::convert::Infallible;
100
101 use rama_core::service::service_fn;
102
103 use super::*;
104 use crate::{BodyExtractExt as _, headers::HeaderMapExt as _};
105 use rama_net::uri::{DEFAULT_DATA_MEDIA_TYPE, Uri};
106
107 fn service() -> DataUriService<impl Service<Request, Output = Response, Error = Infallible>> {
108 DataUriService::new(service_fn(async |_: Request| {
109 Ok::<_, Infallible>(Response::new(Body::from("remote")))
110 }))
111 }
112
113 async fn get(uri: &str) -> Result<Response, BoxError> {
114 let uri: Uri = uri.parse().unwrap();
115 service()
116 .serve(Request::get(uri).body(Body::empty()).unwrap())
117 .await
118 }
119
120 fn content_type(resp: &Response) -> crate::mime::Mime {
121 resp.headers()
122 .typed_get::<ContentType>()
123 .unwrap()
124 .into_mime()
125 }
126
127 #[tokio::test]
128 async fn non_data_scheme_passes_through() {
129 let resp = get("http://example.com/x").await.unwrap();
130 assert_eq!(resp.try_into_string().await.unwrap(), "remote");
131 }
132
133 #[tokio::test]
134 async fn plain_payload() {
135 let resp = get("data:,hello%20world").await.unwrap();
136 assert_eq!(resp.status(), StatusCode::OK);
137 assert_eq!(
138 content_type(&resp),
139 DEFAULT_DATA_MEDIA_TYPE
140 .parse::<crate::mime::Mime>()
141 .unwrap()
142 );
143 assert_eq!(resp.try_into_string().await.unwrap(), "hello world");
144 }
145
146 #[tokio::test]
147 async fn base64_payload_with_media_type() {
148 let resp = get("data:text/javascript;base64,RElSRUNU").await.unwrap();
149 assert_eq!(content_type(&resp), crate::mime::TEXT_JAVASCRIPT);
150 assert_eq!(resp.try_into_string().await.unwrap(), "DIRECT");
151 }
152
153 #[tokio::test]
154 async fn media_type_is_preserved() {
155 let resp = get("data:application/x-ns-proxy-autoconfig,DIRECT")
156 .await
157 .unwrap();
158 assert_eq!(
159 content_type(&resp).as_ref(),
160 "application/x-ns-proxy-autoconfig"
161 );
162 }
163
164 #[tokio::test]
165 async fn payload_keeps_query_and_drops_fragment() {
166 let resp = get("data:application/x-ns-proxy-autoconfig,return h==host?DIRECT:PROXY")
168 .await
169 .unwrap();
170 assert_eq!(
171 resp.try_into_string().await.unwrap(),
172 "return h==host?DIRECT:PROXY"
173 );
174
175 let resp = get("data:,a?b#c").await.unwrap();
176 assert_eq!(resp.try_into_string().await.unwrap(), "a?b");
177 }
178
179 #[tokio::test]
180 async fn head_has_no_body() {
181 let resp = service()
182 .serve(
183 Request::builder()
184 .method(Method::HEAD)
185 .uri("data:,hello")
186 .body(Body::empty())
187 .unwrap(),
188 )
189 .await
190 .unwrap();
191 assert_eq!(resp.try_into_string().await.unwrap(), "");
192 }
193
194 #[tokio::test]
195 async fn missing_comma_errors() {
196 let _err = get("data:text/plain").await.unwrap_err();
197 }
198
199 #[tokio::test]
200 async fn invalid_media_type_errors() {
201 let _err = get("data:not a mime,payload").await.unwrap_err();
202 }
203
204 #[tokio::test]
205 async fn invalid_base64_errors() {
206 let _err = get("data:text/plain;base64,!!!not-base64!!!")
207 .await
208 .unwrap_err();
209 }
210
211 #[tokio::test]
212 async fn rejects_non_get_methods() {
213 let err = service()
214 .serve(
215 Request::builder()
216 .method(Method::POST)
217 .uri("data:,x")
218 .body(Body::empty())
219 .unwrap(),
220 )
221 .await
222 .unwrap_err();
223 assert!(err.to_string().contains("GET and HEAD"), "{err}");
224 }
225}