ntex-rs / ntex

framework for composable networking services
Apache License 2.0
1.84k stars 105 forks source link

Can't access state in middleware #350

Closed johnpmitsch closed 1 month ago

johnpmitsch commented 1 month ago

Forgive me if I'm missing something obvious here, but I can't access state (app_data) in middleware, is there a way to do this? Here is what I'm trying:

use ntex::service::{Middleware, Service, ServiceCtx};
use ntex::web::{types::State, Error, WebRequest, WebResponse};
use std::sync::Arc;

// Define a mock metrics recording function and state for demonstration purposes
mod metrics {
    use std::sync::Mutex;

    pub struct MetricsState {
        pub http_request_counter: Mutex<u64>,
    }

    pub fn record_http_request_metrics(status: &str, counter: &Mutex<u64>) {
        let mut num = counter.lock().unwrap();
        *num += 1;
        println!("Status: {}, Total Requests: {}", status, num);
    }
}

// Middleware for HTTP Metrics Logging
pub struct HttpMetrics;

impl<S> Middleware<S> for HttpMetrics {
    type Service = HttpMetricsMiddleware<S>;

    fn create(&self, service: S) -> Self::Service {
        HttpMetricsMiddleware { service }
    }
}

pub struct HttpMetricsMiddleware<S> {
    service: S,
}

impl<S, Err> Service<WebRequest<Err>> for HttpMetricsMiddleware<S>
where
    S: Service<WebRequest<Err>, Response = WebResponse, Error = Error>,
{
    type Response = WebResponse;
    type Error = Error;

    ntex::forward_poll_ready!(service);
    ntex::forward_poll_shutdown!(service);

    async fn call(
        &self,
        req: WebRequest<Err>,
        ctx: ServiceCtx<'_, Self>,
    ) -> Result<Self::Response, Self::Error> {
        // Attempt to access shared application state
        let metrics_state = req.app_state::<State<Arc<metrics::MetricsState>>>();

        // Error is here: Diagnostics:
        // 1. cannot move out of `req` because it is borrowed
        // move out of `req` occurs here [E0505]
        let res = ctx.call(&self.service, req).await?;
        // Record metrics if the state was successfully retrieved
        match metrics_state {
            Some(state) => {
                metrics::record_http_request_metrics(
                    &res.status().to_string(),
                    &state.http_request_counter
                );
            },
            None => todo!("Handle the absence of shared state"),
        }

        println!("Hi from response {}", res.status());
        Ok(res)
    }
}
fafhrd91 commented 1 month ago

you don't need State<>, type has to be the same as you registered with App::state()

johnpmitsch commented 1 month ago

@fafhrd91 A key line was commented out, i just fixed it, the issue is more trying to appease the borrow checker, using req twice, first to access the state, and then in ctx.call

        // Error is here: Diagnostics:
        // 1. cannot move out of `req` because it is borrowed
        // move out of `req` occurs here [E0505]
        let res = ctx.call(&self.service, req).await?;
fafhrd91 commented 1 month ago

if you wrap state in Arc, then just clone it

johnpmitsch commented 1 month ago

@fafhrd91 sorry, I think I'm missing the fix here, I made a reproducer here https://github.com/johnpmitsch/ntex_issue_repro/blob/main/src/main.rs - how am I supposed to clone the app data if I can't borrow req?

fafhrd91 commented 1 month ago

app_state() returns Option<&T>, you need to do

let metrics_state = req.app_state::<Arc<metrics::MetricsState>>().cloned();
johnpmitsch commented 1 month ago

Thank you! .cloned() is what I was missing, really appreciate the help 🙏🏻