paperclip icon indicating copy to clipboard operation
paperclip copied to clipboard

paperclip in actix, doesn't work when we use web::scope

Open omid opened this issue 4 years ago • 6 comments

The following code doesn't work, because of the web:scope in the router function. There is no compile error, the response to "/api/spec" is 404.

use actix_web::{App, HttpServer};
use paperclip::actix::{
    OpenApiExt, Apiv2Schema, api_v2_operation,
    web::{self, Json},
};
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Apiv2Schema)]
struct Pet {
    name: String,
    id: Option<i64>,
}

#[api_v2_operation]
async fn echo_pet(body: Json<Pet>) -> Result<Json<Pet>, ()> {
    Ok(body)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new()
        .wrap_api()
        .configure(router)
        .with_json_spec_at("/api/spec")
        .build()
    ).bind("127.0.0.1:8080")?
        .run().await
}

pub fn router(service_config: &mut web::ServiceConfig) {
    // this one works
    // service_config
    //     .service(
    //     web::resource("/pets")
    //         .route(web::post().to(echo_pet))
    // );

    // but this one doesn't
    service_config
        .service(
        web::scope("/")
            // with this
            .route("/pets", web::get().to(echo_pet))
            // or this
            // .service(
            //     web::resource("/pets").to(echo_pet)
            // )
    );
}

My Cargo.toml is like:

[dependencies]
actix-web = "3.1"
serde = "1.0"

[dependencies.paperclip]
git = "https://github.com/wafflespeanut/paperclip.git"
features = ["actix"]

I couldn't find where exactly some codes are missing, otherwise I might be able to fix it :( Maybe you can lead me.

omid avatar Oct 01 '20 09:10 omid

I think I might have been having the same problem as you, and seeing your issue here helped me to find it. I don't think it is a bug with paperclip. I think that what actix does when you specify a scope is direct everything under that path to that scope. In your case you have the scope / and so no requests are handled by the paperclip spec service.

I believe that you can use a scope as long as it doesn't overlap with the spec route, eg.

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new()
        .wrap_api()
        .configure(router)
        .with_json_spec_at("/spec")
        .build()
    ).bind("127.0.0.1:8080")?
        .run().await
}

pub fn router(service_config: &mut web::ServiceConfig) {
    service_config
        .service(
        web::scope("/api")
            .route("/pets", web::get().to(echo_pet))
    );
}

platy avatar Oct 01 '20 13:10 platy

You are right. The second route here doesn't work in actix:

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

pub fn router(service_config: &mut web::ServiceConfig) {
    service_config
        .service(
        web::scope("/api")
            .route("/first", web::get().to(echo_pet))
    ).service(
        web::scope("/api")
            .route("/second", web::get().to(echo_pet))
    );
}

The problem is that in my case I have to have scope("/") because I want to .configure several times via ServiceConfig and the only way I found is to have a scope!

I may add more text to this issue in the coming days.

omid avatar Oct 02 '20 10:10 omid

I don't think you need scope to use configure, it's just if you have some common path prefix and want those routes to have the some of the same behaviour. Can you do something like this without scope?:

pub fn router(service_config: &mut web::ServiceConfig) {
    service_config
        .route("/pets", web::get().to(echo_pet))
        .route("/pets2", web::get().to(echo_pet2));
}

platy avatar Oct 02 '20 10:10 platy

Yes @platy, that works. But I want to .configure ServiceConfig. Just imagine something like the following code (check the router function):

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new()
        .wrap_api()
        .configure(router)
        .with_json_spec_at("/spec")
        .build()
    ).bind("127.0.0.1:8080")?
        .run().await
}

pub fn router(service_config: &mut web::ServiceConfig) {
    service_config
        .service(
            web::scope("/")
                .service(resource("").to(echo_pet))
                .configure(subrouter1)
                .configure(subrouter2)
        );
}

pub fn subrouter1(cfg: &mut web::ServiceConfig) {
    cfg
        .service(
            resource("/b")
                .route(web::get().to(echo_pet))
        );
}

pub fn subrouter2(cfg: &mut web::ServiceConfig) {
    cfg
        .service(
            resource("/c")
                .route(web::get().to(echo_pet))
        );
}

omid avatar Oct 02 '20 11:10 omid

I found another problem in the same area!

The following code doesn't work, because we have a path normalizer! If you comment the Normalizer middleware, it will work!

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new()
        .wrap(middleware::NormalizePath::default())
        .wrap_api()
        .configure(router)
        .with_json_spec_at("/spec")
        .build()
    ).bind("127.0.0.1:8080")?
        .run().await
}

pub fn router(service_config: &mut web::ServiceConfig) {
    service_config
        .service(
            web::scope("/api")
                .route("/pets", web::get().to(echo_pet))
        );
}

But it does work when I have .wrap(middleware::NormalizePath::new(TrailingSlash::Trim))

omid avatar Oct 02 '20 11:10 omid

Yes @platy, that works. But I want to .configure ServiceConfig. Just imagine something like the following code (check the router function): ...

Aha! Now I get your issue - thanks.

platy avatar Oct 02 '20 12:10 platy