Problem with adding DB to actix bearer validator

Hey!

I am currently trying to implement a web service, which is based on actix_web 4 and use actix_web_httpauth and jsonwebtoken for the JWT handling and sea_orm as a ORM for Postgres. My code looks similar to this howto. My problem is that my validator function needs to do a check if a specific entry in the database exists. So I have to pass the DatabaseConnection to the validator.

My database connection looks like this:

let db: DatabaseConnection = Database::connect(&config.database_url)
    .await
    .expect("Database error");

The following auth ...

HttpServer::new(move || {
  let auth = HttpAuthentication::bearer(auth_token);
  App::new()
      .wrap(auth)
      .app_data(actix_web::web::Data::new(db.clone()))
      ...
})

... could be rewritten as HttpAuthentication::bearer(|req, cred| auth_token (req, cred)) and I could add db, like this:

HttpServer::new(move || {
  let auth = HttpAuthentication::bearer(|req, cred| auth_token (req, cred, db.clone()));;
  App::new()
      .wrap(auth)
      ...
})

async fn auth_token(req: ServiceRequest, credentials: BearerAuth, db: DatabaseConnection) -> Result<ServiceRequest, Error> {
  ...
}

If I do this I get the following error:

49 |     HttpServer::new(move || {
   |                     ------- lifetime `'1` represents this closure's body
50 |         let auth = HttpAuthentication::bearer(|req, cred| auth_token (req, cred, db.clone()));
   |                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ argument requires that `'1` must outlive `'static`
   |
   = note: closure implements `Fn`, so references to captured variables can't escape the closure

I tried to wrap db inside Arc/Mutex but it still doesn't work.

Am I doing something here which is fundamentally wrong or what knowledge am I missing in Rust to handle these kind of problems?

BR,
Casi

Have you tried to create you auth variable outside of the HttpServer closure ?

Hi @peter_rabbit ! This results in:

48 |     let auth = HttpAuthentication::bearer(|req, cred| auth_token (req, cred, db.clone()));
   |                --------------------------------------------------------------------------
   |                |                          |                                  |
   |                |                          |                                  borrow occurs due to use in closure
   |                |                          borrow of `db` occurs here
   |                argument requires that `db` is borrowed for `'static`
49 |     HttpServer::new(move || {
   |                     ^^^^^^^ move out of `db` occurs here
...
53 |             .app_data(actix_web::web::Data::new(db.clone()))
   |                                                 -- move occurs due to use in closure

and I would"ve thought that ServiceRequest and BearerAuth are only available within HttpServer

Then you could apply the same logic for db and create the clone outside of the closure where the borrowing occurs. But as you will have to access the db connection in multiple places, then a better idea would be to use the fact that you pass it in an Arc (Data) in the app_data.

The middleware req parameter allows you to retrieve the app_data, like in any service:

let auth = HttpAuthentication::bearer(|req, cred| {
    let db = req
        .app_data::<actix_web::web::Data<DatabaseConnection>>()
        .expect("Failed to extract DatabaseConnection from ServiceRequest");
    auth_token (req, cred, db)
});

I couldn't test it because I don't have the context so maybe you'd have to adapt a little but I have done similar things already.

Hey! This didn't work either :confused:

#[actix_web::main]
pub async fn main() -> std::io::Result<()> {
    env_logger::init_from_env(env_logger::Env::new().default_filter_or("debug"));
    let config = get_config().await;

    info!(
        "Starting HTTP server at http://{}:{}",
        &config.bind_addr, &config.bind_port
    );

    let db = Database::connect(&config.database_url)
        .await
        .expect("Database error");

    let _ = migration::initialize_database(&db).await;
    
    HttpServer::new(move || {  
        let auth = HttpAuthentication::bearer(|req, cred| {
            let db = req
                .app_data::<actix_web::web::Data<DatabaseConnection>>()
                .expect("Failed to extract DatabaseConnection from ServiceRequest");
            auth_token (req, cred, db.clone())
        });
        App::new()
            ...
    })
    .bind(format!("{}:{}", &config.bind_addr, &config.bind_port))?
    .run()
    .await
}

pub async fn auth_token(
    req: ServiceRequest,
    bearer: BearerAuth,
    db: actix_web::web::Data<sea_orm::DatabaseConnection>,
) -> Result<ServiceRequest, (Error, ServiceRequest)> {
    ...
}

Error is:

error[E0505]: cannot move out of `req` because it is borrowed
  --> server-api/src/lib.rs:58:25
   |
55 |               let db = req
   |  ______________________-
56 | |                 .app_data::<actix_web::web::Data<DatabaseConnection>>()
   | |_______________________________________________________________________- borrow of `req` occurs here
57 |                   .expect("Failed to extract DatabaseConnection from ServiceRequest");
58 |               auth_token (req, cred, db.clone())
   |                           ^^^        ---------- borrow later used here
   |                           |
   |                           move out of `req` occurs here

Maybe you can extract your db connection from the request inside the auth_token function? That way you don't have to borrow req twice.

Yes! Your previous solution worked!

let auth = HttpAuthentication::bearer(|req, cred| {
    let db = req
        .app_data::<actix_web::web::Data<DatabaseConnection>>()
        .expect("Failed to extract DatabaseConnection from ServiceRequest")
        .get_ref()
        .clone();
    auth_token(req, cred, db)
});

Now I can access the database within the validator function. Thank you very much for your support and help!!

2 Likes

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.