simbld_http/helpers/
http_interceptor_helper.rs

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