Actix Web: Adding redirects programmatically

I'm building an Actix-Web app to serve the contents of a directory. I'm trying to use all items from a HashMap in the App to use as redirects:


use actix_files::{Files, NamedFile};
use actix_web::http::StatusCode;
use actix_web::{web, App};
use actix_web::{HttpRequest, HttpServer, Responder};
use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

#[derive(Deserialize, Clone, Debug)]
struct RawRedirects(HashMap<String, RawRedirectValue>);

#[derive(Clone, Debug)]
struct Redirects(HashMap<String, RedirectValue>);

#[derive(Deserialize, Clone, Debug)]
struct RawRedirectValue {
    status: u16,
    destination: String,
}

#[derive(Clone, Debug)]
struct RedirectValue {
    status: http::StatusCode,
    destination: String,
}

#[derive(Clone, Debug)]
struct AppData {
    not_found_file: PathBuf,
}

impl From<RawRedirects> for Redirects {
    fn from(val: RawRedirects) -> Self {
        val.0
            .into_iter()
            .map(|(key, value)| {
                (
                    key.trim_end_matches('/').to_string(),
                    RedirectValue {
                        status: StatusCode::from_u16(value.status).unwrap(),
                        destination: value.destination.trim_end_matches('/').to_string(),
                    },
                )
            })
            .collect()
    }
}

impl std::iter::FromIterator<(std::string::String, RedirectValue)> for Redirects {
    fn from_iter<T: IntoIterator<Item = (std::string::String, RedirectValue)>>(iter: T) -> Self {
        let mut c: Redirects = Redirects(HashMap::new());

        for i in iter {
            c.0.insert(i.0, i.1);
        }

        c
    }
}

#[actix_web::main]
async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
    let dir_to_serve: PathBuf = std::env::var("DIR_TO_SERVE")
        .unwrap_or("dist".into())
        .into();

    let port: u16 = std::env::var("PORT")
        .unwrap_or("5173".into())
        .parse()
        .unwrap();

    let interface: String = std::env::var("INTERFACE_TO_BIND").unwrap_or("127.0.0.1".into());

    let not_found_file: PathBuf = std::env::var("NOT_FOUND_FILE")
        .unwrap_or(
            Path::new("dist")
                .join("404.html")
                .to_str()
                .unwrap()
                .to_string(),
        )
        .parse()
        .unwrap();

    let mount_path = std::env::var("MOUNT_PATH").unwrap_or("/".into());

    let raw_redirects: RawRedirects = {
        let file_content: String = fs::read_to_string("redirects.json")?;
        serde_json::from_str(&file_content)?
    };

    let mut app = App::new()
        .app_data(web::Data::new(AppData {
            not_found_file: not_found_file.clone(),
        }))
        .service(
            Files::new(&mount_path, &dir_to_serve)
                .index_file("index.html")
                .redirect_to_slash_directory()
                .use_hidden_files()
                .default_handler(web::to({
                    let value: PathBuf = dir_to_serve.clone();
                    move |req: HttpRequest, app_data: web::Data<AppData>| {
                        serve_index_file(req, value.clone(), app_data)
                    }
                })),
        );

    let redirects: Redirects = raw_redirects.clone().into();

    for (key, value) in redirects.0 {
        app.service(web::Redirect::new(key, value.destination).using_status_code(value.status));
    }

    let server = HttpServer::new(move || app).bind((interface.to_owned(), port))?;

    println!("Docs running on: http://{}:{}", interface, port);

    server.run().await?;

    Ok(())
}

async fn serve_index_file(
    req: HttpRequest,
    mut dir_to_serve: PathBuf,
    app_data: web::Data<AppData>,
) -> impl Responder {
    let path = req.path().trim_start_matches('/');

    dir_to_serve.push(path);
    dir_to_serve.push("index.html");

    match NamedFile::open(dir_to_serve) {
        Ok(named_file) => named_file.respond_to(&req),
        Err(_) => NamedFile::open(app_data.not_found_file.clone())
            .unwrap()
            .customize()
            .with_status(StatusCode::NOT_FOUND)
            .respond_to(&req)
            .map_into_boxed_body(),
    }
}

However i get this error:

the trait bound `actix_web::App<actix_web::app_service::AppEntry>: std::clone::Clone` is not satisfied in `{closure@docs/src/main.rs:116:34: 116:41}`
within `{closure@docs/src/main.rs:116:34: 116:41}`, the trait `std::clone::Clone` is not implemented for `actix_web::App<actix_web::app_service::AppEntry>`, which is required by `{closure@docs/src/main.rs:116:34: 116:41}: std::clone::Clone`

I understand, that this is because I'm modifying app variable inside of a for loop, however I haven't found any alternatives/solutions from either Google or various LLMs.

Any Help is greatly appreciated!
Greetings, Alex

What's the value of having the redirects in a separate file?

It's for serving the output of a the astro framework.

Seems that you can user the services macro for this: services in actix_web - Rust.

You can construct the app inside the callback you pass to HttpServer::new, then you don't have a problem with the fact that App is not Clone:

    let server = HttpServer::new(move || {
        let mut app = App::new()
            .app_data(web::Data::new(AppData {
                not_found_file: not_found_file.clone(),
            }))
            .service(
                Files::new(&mount_path, &dir_to_serve)
                    .index_file("index.html")
                    .redirect_to_slash_directory()
                    .use_hidden_files()
                    .default_handler(web::to({
                        let value: PathBuf = dir_to_serve.clone();
                        move |req: HttpRequest, app_data: web::Data<AppData>| {
                            serve_index_file(req, value.clone(), app_data)
                        }
                    })),
            );

        let redirects: Redirects = raw_redirects.clone().into();

        for (key, value) in redirects.0 {
            app = app.service(
                web::Redirect::new(key, value.destination).using_status_code(value.status),
            );
        }

        app
    })
    .bind((interface.clone(), port))?;

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.