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