actix / actix-web

Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust.
https://actix.rs
Apache License 2.0
21.6k stars 1.67k forks source link

app_data not accessible when registered inside a scope #1508

Closed pfrenssen closed 4 years ago

pfrenssen commented 4 years ago

I would like to render my error pages in HTML format using Tera templates. The Tera engine is added to my application using app_data (ref. full code at https://github.com/pfrenssen/firetrack/blob/a500519748aa1e3ad52ecf9fff832d127df8ce27/web/src/lib.rs#L73).

pub async fn serve(config: AppConfig) -> Result<(), String> {
    // Configure the application.
    let app = move || {
        App::new()
            .configure(|c| configure_application(c))
    };

    // ...
}

pub fn configure_application(config: &mut web::ServiceConfig) {
    // I am registering 3 extensions here.
    let tera = tera::Tera::new("templates/**/*").unwrap();
    let pool = db::create_connection_pool(&config.database_url()).unwrap();
    let app_config = get_config();

    config.service(
        web::scope("")
            .data(tera)
            .data(pool)
            .data(app_config)
            .wrap(error::error_handlers())
            .route("/", web::get().to(index))
    );
}

I have an error handler defined as such:

pub fn error_handlers() -> ErrorHandlers<Body> {
    ErrorHandlers::new()
        .handler(StatusCode::NOT_FOUND, not_found)
}

fn not_found<B>(res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
    let request = res.request();
    println!("Tera data {:?}", request.app_data::<Data<Tera>>());
    println!("Tera {:?}", request.app_data::<Tera>());

    let response = Response::build(res.status())
        .content_type("text/html")
        .body("Page not found");
    Ok(ErrorHandlerResponse::Response(
        res.into_response(response.into_body()),
    ))
}

I seem to be unable to access the app_data in the error handler. When I access a 404 page I see the following log output:

Tera data None
Tera None

Moreover, it seems that the app data is completely empty, if I hack the following debugging info in extensions.rs then it appears that the Extensions are empty:

pub fn get<T: 'static>(&self) -> Option<&T> {
    println!("map len {:?}", self.map.len());
    self.map
        .get(&TypeId::of::<T>())
        .and_then(|boxed| (&**boxed as &(dyn Any + 'static)).downcast_ref())
}

This results in the following log output, which seems to indicate that none of my 3 registered extensions are available here:

map len 0
Tera data None
map len 0
Tera None

Is there a way that we can currently access the app_data in an error handler, or if this needs to be implemented, where should this be done?

pfrenssen commented 4 years ago

I found that this minimal example does work, this will find the TEST string in the .app_data() passed from within an error handler.

use actix_http::{body::Body, Response};
use actix_web::dev::ServiceResponse;
use actix_web::http::StatusCode;
use actix_web::middleware::errhandlers::{ErrorHandlerResponse, ErrorHandlers};
use actix_web::{web::Data, App, HttpServer, Result};

#[actix_rt::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new()
        .data("TEST".to_string())
        .wrap(error_handlers()))
        .bind("127.0.0.1:8088")?
        .run()
        .await
}

pub fn error_handlers() -> ErrorHandlers<Body> {
    ErrorHandlers::new().handler(StatusCode::NOT_FOUND, not_found)
}

fn not_found<B>(res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
    let request = res.request();
    let content = request.app_data::<Data<String>>().unwrap().get_ref();
    let response = Response::build(res.status()).body(content);
    Ok(ErrorHandlerResponse::Response(
        res.into_response(response.into_body()),
    ))
}

I will narrow down the problem to see what causes the data to be missing in my example in the summary. It might be due to the data being registered inside web::scope() as was suggested on gitter.

pfrenssen commented 4 years ago

I found it, it is indeed due to web::scope().

Moving the app configuration into a second method, and wrapping the .data() call into .scope("") makes the value inaccessible. Replacing main() with the following causes the data to be inaccessible:

#[actix_rt::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new().configure(|c| conf(c)))
        .bind("127.0.0.1:8088")?
        .run()
        .await
}

fn conf(c: &mut ServiceConfig) {
    c.service(scope("")
        .data("TEST".to_string()) // <<<< Data inside `scope()` is inaccessible
        .wrap(error_handlers()));
}

This small change makes the data accessible again:

fn conf(c: &mut ServiceConfig) {
    c.data("TEST".to_string())
        .service(scope("").wrap(error_handlers()));
}
robjtede commented 4 years ago

want to find out if this is a bug or not

robjtede commented 4 years ago

Found the culprit. The usage of the error middleware was a red herring here.

The problem is an already fixed issue whereby default services are not given current-level data. Fixed here: #1452.

If you try your code using actix-web v3 alpha you'll find it works.