rama_http/layer/match_redirect/
service.rs1use crate::service::web::response::{Headers, IntoResponse};
2use crate::{Body, Request, Response, StatusCode, StreamingBody};
3use rama_core::Service;
4use rama_core::bytes::Bytes;
5use rama_core::error::BoxError;
6use rama_core::telemetry::tracing;
7use rama_http_headers::Location;
8use rama_net::http::uri::{UriMatchError, UriMatchReplace};
9use rama_utils::macros::define_inner_service_accessors;
10use std::borrow::Cow;
11
12#[derive(Debug, Clone)]
20pub struct UriMatchRedirectService<R, S> {
21 status_code: StatusCode,
22 match_replace: R,
23 inner: S,
24}
25
26impl<R, S> UriMatchRedirectService<R, S> {
27 #[inline]
33 pub fn to(match_replace: R, service: S) -> Self {
34 Self::new(StatusCode::SEE_OTHER, match_replace, service)
35 }
36
37 #[inline]
43 pub fn moved(match_replace: R, service: S) -> Self {
44 Self::new(StatusCode::MOVED_PERMANENTLY, match_replace, service)
45 }
46
47 #[inline]
53 pub fn found(match_replace: R, service: S) -> Self {
54 Self::new(StatusCode::FOUND, match_replace, service)
55 }
56
57 #[inline]
63 pub fn temporary(match_replace: R, service: S) -> Self {
64 Self::new(StatusCode::TEMPORARY_REDIRECT, match_replace, service)
65 }
66
67 #[inline]
73 pub fn permanent(match_replace: R, service: S) -> Self {
74 Self::new(StatusCode::PERMANENT_REDIRECT, match_replace, service)
75 }
76
77 pub(super) fn new(status_code: StatusCode, match_replace: R, service: S) -> Self {
78 Self {
79 status_code,
80 match_replace,
81 inner: service,
82 }
83 }
84}
85
86impl<R, S> UriMatchRedirectService<R, S> {
87 define_inner_service_accessors!();
88
89 #[must_use]
91 pub fn match_replace_ref(&self) -> &R {
92 &self.match_replace
93 }
94
95 #[must_use]
97 pub fn match_replace_mut(&mut self) -> &mut R {
98 &mut self.match_replace
99 }
100}
101
102impl<ReqBody, ResBody, R, S> Service<Request<ReqBody>> for UriMatchRedirectService<R, S>
103where
104 S: Service<Request<ReqBody>, Output = Response<ResBody>>,
105 R: UriMatchReplace + Send + Sync + 'static,
106 ReqBody: Send + 'static,
107 ResBody: StreamingBody<Data = Bytes, Error: Into<BoxError>> + Send + Sync + 'static,
108{
109 type Output = Response;
110 type Error = S::Error;
111
112 async fn serve(&self, req: Request<ReqBody>) -> Result<Self::Output, Self::Error> {
113 let full_uri = req.request_uri();
114 if let Ok(uri) = self
115 .match_replace
116 .match_replace_uri(Cow::Owned(full_uri.clone()))
117 .inspect_err(|err| match err {
118 UriMatchError::NoMatch(uri) => {
119 tracing::trace!("no match found for uri: {uri}; ignore")
120 }
121 UriMatchError::Unexpected(err) => {
122 tracing::trace!("unexpected error while trying to match uri: {err}; ignore")
123 }
124 })
125 && *uri != full_uri
126 {
127 return match Location::try_from(uri.as_ref()) {
128 Ok(loc) => {
129 tracing::debug!(
130 "redirct request '{full_uri}' to '{uri}' w/ status code {}",
131 self.status_code
132 );
133 Ok((Headers::single(loc), self.status_code).into_response())
134 }
135 Err(err) => {
136 tracing::debug!(
137 "failed to send response for redirct request '{full_uri}' to '{uri}' w/ status code {}; loc header encoding failed: {err}",
138 self.status_code
139 );
140 Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response())
141 }
142 };
143 }
144
145 let resp = self.inner.serve(req).await?;
146 Ok(resp.map(Body::new))
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use crate::{layer::into_response::IntoResponseService, service::web::IntoEndpointService};
153 use rama_http_headers::HeaderMapExt;
154 use rama_net::http::uri::UriMatchReplaceRule;
155 use rama_net::uri::Uri;
156
157 use super::*;
158
159 #[tokio::test]
160 async fn test_redirect_svc() {
161 let svc = UriMatchRedirectService::moved(
162 [
163 UriMatchReplaceRule::http_to_https(),
164 UriMatchReplaceRule::try_new("https://www.*", "https://$1").unwrap(),
165 UriMatchReplaceRule::try_new("*", "$1").unwrap(), ],
167 IntoResponseService::new(StatusCode::OK.into_endpoint_service()),
168 );
169
170 for (input_uri, expected_option) in [
171 ("http://example.com", Some("https://example.com")),
172 ("http://example.com/foo", Some("https://example.com/foo")),
173 ("https://www.example.com", Some("https://example.com")),
174 (
175 "https://www.example.com/foo",
176 Some("https://example.com/foo"),
177 ),
178 ("https://example.com", None),
179 ("https://example.com/foo", None),
180 ] {
181 let req = Request::builder()
182 .uri(input_uri)
183 .body(Body::empty())
184 .unwrap();
185 let resp = svc.serve(req).await.unwrap();
186 match expected_option {
187 Some(loc) => {
188 assert_eq!(StatusCode::MOVED_PERMANENTLY, resp.status());
189 assert_eq!(
190 resp.headers()
191 .typed_get::<Location>()
192 .and_then(|loc| loc.to_str().ok().and_then(|s| Uri::try_from(s).ok())),
193 Some(Uri::from_static(loc)),
194 );
195 }
196 None => {
197 assert_eq!(StatusCode::OK, resp.status());
198 }
199 }
200 }
201 }
202}