simbld_http/helpers/
http_interceptor_helper.rs

1/// Intercepts HTTP requests and responses to add custom headers.
2///
3/// Purpose: Intercept requests and responses to add cross-functional functionality such as logging.
4/// How it works: It intercepts every request and response, allowing headers to be added or information to be logged.
5/// Special feature: It uses Rc to share the service between the different middleware instances.
6///
7/// This struct implements the `Service` trait, allowing it to intercept
8/// HTTP requests and responses. It adds the following headers to the response:
9/// - `x-request-id`: A unique identifier for the request.
10/// - `x-response-time-ms`: The time taken to process the request in milliseconds.
11/// - `x-status-description`: A description of the status code, if available.
12///
13/// # Types
14/// - `Response`: The type of the response, which is `ServiceResponse<B>`.
15/// - `Error`: The type of the error, which is `Error`.
16/// - `Future`: The type of the future, which is `LocalBoxFuture<'static, Result<Self::Response, Self::Error>>`.
17///
18/// # Methods
19/// - `poll_ready`: Checks if the service is ready to accept a request.
20/// - `call`: Intercepts the request, processes it, and adds custom headers to the response.
21///
22/// # Arguments
23/// - `cx`: The context for the poll_ready method.
24/// - `req`: The service request to be intercepted.
25///
26/// # Returns
27/// - `poll_ready`: A `Poll` indicating if the service is ready.
28/// - `call`: A future that resolves to the intercepted response with custom headers.
29use 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}