simbld_http/helpers/
http_interceptor_helper.rs1use actix_service::{Service, Transform};
30use actix_web::dev::{ServiceRequest, ServiceResponse};
31use actix_web::http::header::{HeaderName, HeaderValue};
32use actix_web::Error;
33use futures_util::future::{ok, LocalBoxFuture, Ready};
34use std::rc::Rc;
35use std::task::{Context, Poll};
36
37#[derive(Debug)]
38pub struct HttpInterceptor;
39
40impl<S, B> Transform<S, ServiceRequest> for HttpInterceptor
41where
42 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
43 B: 'static,
44{
45 type Response = ServiceResponse<B>;
46 type Error = Error;
47 type Transform = HttpInterceptorMiddleware<S>;
48 type InitError = ();
49 type Future = Ready<Result<Self::Transform, Self::InitError>>;
50
51 fn new_transform(&self, service: S) -> Self::Future {
52 ok(HttpInterceptorMiddleware { service: Rc::new(service) })
53 }
54}
55
56pub struct HttpInterceptorMiddleware<S> {
57 service: Rc<S>,
58}
59
60impl<S, B> Service<ServiceRequest> for HttpInterceptorMiddleware<S>
61where
62 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
63 B: 'static,
64{
65 type Response = ServiceResponse<B>;
66 type Error = Error;
67 type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
68
69 fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
70 self.service.poll_ready(cx)
71 }
72
73 fn call(&self, req: ServiceRequest) -> Self::Future {
74 let service = Rc::clone(&self.service);
75 let fut = service.call(req);
76 let start_time = std::time::Instant::now();
77 let request_id = uuid::Uuid::new_v4().to_string();
78
79 Box::pin(async move {
80 let mut res = fut.await?;
81
82 res.headers_mut().insert(
83 HeaderName::from_static("x-request-id"),
84 HeaderValue::from_str(&request_id).unwrap(),
85 );
86
87 let duration = start_time.elapsed().as_millis();
88 res.headers_mut().insert(
89 HeaderName::from_static("x-response-time-ms"),
90 HeaderValue::from_str(&duration.to_string()).unwrap(),
91 );
92
93 let status_code = res.status().as_u16();
94 if let Some(description) =
95 crate::helpers::response_helpers::get_description_by_code(status_code).as_ref()
96 {
97 res.headers_mut().insert(
98 HeaderName::from_static("x-status-description"),
99 HeaderValue::from_str(description).unwrap(),
100 );
101 }
102
103 Ok(res)
104 })
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use actix_web::{test, web, App, HttpResponse};
112
113 #[actix_web::test]
114 async fn test_http_interceptor() {
115 let app = test::init_service(
116 App::new()
117 .wrap(HttpInterceptor)
118 .route("/", web::get().to(|| async { HttpResponse::Ok().body("Hello World") })),
119 )
120 .await;
121
122 let req = test::TestRequest::with_uri("/").to_request();
123 let resp = test::call_service(&app, req).await;
124
125 assert_eq!(resp.status(), actix_web::http::StatusCode::OK);
126 let body = test::read_body(resp).await;
127 assert_eq!(body, "Hello World");
128 }
129
130 #[actix_web::test]
131 async fn test_http_interceptor_adds_header() {
132 let app = test::init_service(
133 App::new()
134 .wrap(HttpInterceptor)
135 .route("/", web::get().to(|| async { HttpResponse::Ok().finish() })),
136 )
137 .await;
138
139 let req = test::TestRequest::with_uri("/").to_request();
140 let resp = test::call_service(&app, req).await;
141
142 assert_eq!(resp.status(), actix_web::http::StatusCode::OK);
143 let header_value = resp
144 .headers()
145 .get("x-status-description")
146 .expect("Header 'x-status-description' is missing")
147 .to_str()
148 .unwrap();
149 assert_eq!(
150 header_value,
151 "Request processed successfully. Response will depend on the request method used, and the result will be either a representation of the requested resource or an empty response"
152 );
153 }
154}