juhaku / utoipa

Simple, Fast, Code first and Compile time generated OpenAPI documentation for Rust
Apache License 2.0
2.48k stars 197 forks source link

utopia-actix-web Scope is not working as shown in the Example #1212

Closed Letgamer closed 22 hours ago

Letgamer commented 2 days ago

Hey there, I am currently building a password manager backend API, here is the source code: https://github.com/Letgamer/rsPass

I have the following problem, and hopefully it isn't a dumb error on my side😅:

I have in the main.rs:

.into_utoipa_app()
            .service(route_health)
            .service(route_email)
            .service(route_login)
            .service(route_register)
            .service(
                scope("/api/v1/accounts")
                    .wrap(auth.clone())
                    .route("/changepwd", web::post().to(route_changepwd))
                    .route("/logout", web::get().to(route_logout))
                    .route("/delete", web::get().to(route_delete))
            )
            .service(
                scope("/api/v1/sync")
                    .wrap(auth)
                    .route("/fetch", web::get().to(route_fetch))
                    .route("/update", web::post().to(route_update))
            )
            .split_for_parts();

and in the routes.rs I have:

#[utoipa::path(
    post,
    path = "/api/v1/accounts/changepwd",
    request_body = ChangeRequest,
    responses(
        (status = 200, description = "Password changed successfully!"),
        (status = 400, description = "Invalid payload"),
        (status = 401, description = "JWT Token is invalid"),
        (status = 500, description = "Database Error or JWT Generation Error")
    ),
    tag = "accounts",
    security(
        ("jwt_auth" = [])
    )
)]
pub async fn route_changepwd(req: HttpRequest, req_body: web::Json<ChangeRequest>, auth: BearerAuth) -> impl Responder {
    debug!("authenticated for token: {}", auth.token());
    if let Some(claims) = req.extensions_mut().get::<Claims>() {
        info!("Change Password of: {}", &claims.sub);
        match user_changepwd(&claims.sub, &req_body.password_hash){
            Ok(()) => HttpResponse::Ok().finish(),
            Err(e) => handle_db_error(&e),
        }
    }
    else {
        HttpResponse::Unauthorized().finish()
    }
}

I then wanted to change the routes in the scopes to also automatically determine the path and request type as at the routes outside the scope, I tried using your example and changed the main.rs to:

.into_utoipa_app()
            .service(route_health)
            .service(route_email)
            .service(route_login)
            .service(route_register)
            .service(
                scope::scope("/api/v1/accounts")
                    .wrap(auth.clone())
                    .service(route_changepwd) // changed here!
                    .route("/logout", web::get().to(route_logout))
                    .route("/delete", web::get().to(route_delete))
            )
            .service(
                scope("/api/v1/sync")
                    .wrap(auth)
                    .route("/fetch", web::get().to(route_fetch))
                    .route("/update", web::post().to(route_update))
            )
            .split_for_parts();

And I added the macro to the route in routes.rs:

#[utoipa::path(
    request_body = ChangeRequest,
    responses(
        (status = 200, description = "Password changed successfully!"),
        (status = 400, description = "Invalid payload"),
        (status = 401, description = "JWT Token is invalid"),
        (status = 500, description = "Database Error or JWT Generation Error")
    ),
    tag = "accounts",
    security(
        ("jwt_auth" = [])
    )
)]
#[post("/changepwd")]
pub async fn route_changepwd(req: HttpRequest, req_body: web::Json<ChangeRequest>, auth: BearerAuth) -> impl Responder {
    debug!("authenticated for token: {}", auth.token());
    if let Some(claims) = req.extensions_mut().get::<Claims>() {
        info!("Change Password of: {}", &claims.sub);
        match user_changepwd(&claims.sub, &req_body.password_hash){
            Ok(()) => HttpResponse::Ok().finish(),
            Err(e) => handle_db_error(&e),
        }
    }
    else {
        HttpResponse::Unauthorized().finish()
    }
}

So that is now exactly the same as shown in your README.md: https://github.com/juhaku/utoipa/blob/master/utoipa-actix-web/README.md

The Route is correctly registered under /api/v1/accounts/changepwd but not in the openapi.json/swagger-ui. There the path is: image So the scope is completely ignored.

I would appreciate any help, thanks in advance :)

juhaku commented 1 day ago

Hmm it is quite hard to tell from here what might go wrong but the example here https://github.com/juhaku/utoipa/blob/master/examples/actix-web-scopes-binding/src/main.rs does work. If you just run it it will correctly set the handler under specific scopes.

Even if I add another module here called


mod api3 {
    use actix_web::get;

    #[utoipa::path(
        responses(
            (status = 200, description = "Foobar", body = str)
        )
    )]
    #[get("/foobar")]
    pub(super) async fn foobar() -> &'static str {
        "foobar"
    }
}

And then modify the example App definition as follows, it does work. However the web::get().to(...) does not show up in the openapi.json as that is by design.

.service(
    scope::scope("/api")
        .service(crate::api3::foobar)
        .route("/foo", web::get().to(|| async { "foo" }))
        .route("/bar", web::get().to(|| async { "bar" }))
        .service(scope::scope("/v1").service(api1::hello1))
        .service(scope::scope("/v2").service(api2::hello2)),
)
Letgamer commented 22 hours ago

Hey, I appreciate the Answer, thanks for the help :)

However the web::get().to(...) does not show up in the openapi.json as that is by design.

Just wanna point out that I am aware of that and that it is not my problem, I am talking about the:

.service(
                scope::scope("/api/v1/accounts")
                    .wrap(auth.clone())
                    .service(route_changepwd)

That's from your example, that's the one I mean, so I am specifically talking about the .service that isn't working.

I am closing this issue as it seems more likely to be an issue on my side.

If I figure out the problem, I will post a solution here for the sake of completeness👍