Need Help with Actix Web Middleware and Async Mutex Access

Hello,

I'm seeking assistance with implementing middleware in Actix Web. I'm facing a challenge where I need to access shared state through an asynchronous mutex within a non-async function. Specifically, I'm trying to maintain the logic for redirecting and continuing the execution of middleware based on the state of a database connection.

Here is the part of the code I'm struggling with:

struct StateDb{
    azs_db: Mutex<AzsDb>,
    sqlite: SqlitePool,
}

pub struct AzsDb{
    pub mysql: Option<MySqlPool>,
    pub mysql_info_success: MysqlInfo,
    pub mysql_info_last: MysqlInfo,
    is_connecting: bool,
    pub logs_error: String,
}

fn call(&self, req: ServiceRequest) -> Self::Future {
    println!("Hi from start. You requested: {}", req.path());
    let state = req.app_data::<web::Data<StateDb>>().unwrap();
    let azs_db = state.azs_db.lock().await;
    if azs_db.mysql.is_none() {
        let response = HttpResponse::Found()
            .insert_header((http::header::LOCATION, "/settings/dbproperties"))
            .finish().map_into_right_body();
        Box::pin(async move {
            Ok(ServiceResponse::new(req.into_parts().0, response))
        })
    } else {
        let fut = self.service.call(req);
        
        Box::pin(async move {
            fut.await.map(ServiceResponse::map_into_left_body)
        })
    }
}

The issue I encounter is related to managing asynchronous state access within the middleware's synchronous call function, and integrating this logic with redirect actions. How can I correctly implement the async lock and conditional redirect while conforming to Actix Web's middleware structure?

Any guidance or examples on how to handle this would be greatly appreciated. Thank you in advance for your help.

The function returns a Pin<Box<Future<...>>>. You can wrap all the function body in a Box::pin(async move { ... })), rather than just the ServiceResponses that you return.

In addition to @jofas's approach, I expect that referencing self.service in the async block will result in some lifetime error. If that's actually the case you may consider wrapping the service field in a Rc, and cloning it before the outer Box::pin(async move { ... }).

Can you please provide a code example. Thank

fn call(&self, req: ServiceRequest) -> Self::Future {
    Box::pin(async move {
        println!("Hi from start. You requested: {}", req.path());
        let state = req.app_data::<web::Data<StateDb>>().unwrap();
        let azs_db = state.azs_db.lock().await;
        if azs_db.mysql.is_none() {
            let response = HttpResponse::Found()
                .insert_header((http::header::LOCATION, "/settings/dbproperties"))
                .finish().map_into_right_body();
            Ok(ServiceResponse::new(req.into_parts().0, response))
        } else {
            self.service.call(req).await.map(ServiceResponse::map_into_left_body)
        }
    })
}
1 Like
struct YourMiddleWare<S> {
    service: Rc<S> // instead of just S, as you're probably doing right now
}

// ...

fn call(&self, req: ServiceRequest) -> Self::Future {
    let service = self.service.clone();
    Box::pin(async move {
        // Put everything here, just like @jofas showed
        // However instead of `self.service.call(req)` you'll have to do `service.call(req)`
    })
}

Edit: Just to given an example, the official actix-session crate also uses this pattern actix-extras/actix-session/src/middleware.rs at f250348e573aa67ac9be580037cdaf40648e4903 · actix/actix-extras · GitHub

3 Likes

It gives an error:

error[E0505]: cannot move out of `req` because it is borrowed
  --> src/check_db_middleware.rs:66:41
   |
56 |     fn call(&self, req: ServiceRequest) -> Self::Future {
   |                    --- binding `req` declared here
...
60 |             let state = req.app_data::<web::Data<StateDb>>().unwrap();
   |                         --- borrow of `req` occurs here
...
66 |                 Ok(ServiceResponse::new(req.into_parts().0, response))
   |                                         ^^^ move out of `req` occurs here
...
70 |         })
   |         - borrow might be used here, when `azs_db` is dropped and runs the `Drop` code for type `tokio::sync::MutexGuard`

Everything is a mistake:

let state = req.app_data::<web::Data<StateDb>>().unwrap();
        Box::pin(async move {
            println!("Hi from start. You requested: {}", req.path());
            let azs_db = state.azs_db.lock().await;
            if azs_db.mysql.is_none() {
                let response = HttpResponse::Found()
                    .insert_header((http::header::LOCATION, "/settings/dbproperties"))
                    .finish().map_into_right_body();
                Ok(ServiceResponse::new(req.into_parts().0, response))
            } else {
                self.service.call(req).await.map(ServiceResponse::map_into_left_body)
            }
        })
Everything is a mistake:
let state = req.app_data::<web::Data<StateDb>>().unwrap();
        Box::pin(async move {
            println!("Hi from start. You requested: {}", req.path());
            let azs_db = state.azs_db.lock().await;
            if azs_db.mysql.is_none() {
                let response = HttpResponse::Found()
                    .insert_header((http::header::LOCATION, "/settings/dbproperties"))
                    .finish().map_into_right_body();
                Ok(ServiceResponse::new(req.into_parts().0, response))
            } else {
                self.service.call(req).await.map(ServiceResponse::map_into_left_body)
            }
        })
error: lifetime may not live long enough
  --> src/check_db_middleware.rs:58:9
   |
56 |       fn call(&self, req: ServiceRequest) -> Self::Future {
   |               - let's call the lifetime of this reference `'1`
57 |           let state = req.app_data::<web::Data<StateDb>>().unwrap();
58 | /         Box::pin(async move {
59 | |             println!("Hi from start. You requested: {}", req.path());
60 | |             let azs_db = state.azs_db.lock().await;
61 | |             if azs_db.mysql.is_none() {
...  |
68 | |             }
69 | |         })
   | |__________^ returning this value requires that `'1` must outlive `'static`

I'll have to try it. Thank you. When I check the code I will write

Try putting a drop(azs_db); before the let response

2 Likes

Don't work

Oh you probably need it in the other branch too.

Thank you very much for this example. I will try it

Everything works. A huge thank you @jofas and @SkiFire13 . I added drop(azs_db) and "Rc<S>" and also added to "+'static" S: Service<ServiceRequest, Response = ServiceResponse, Error = Error>+ 'static

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.