salvo-rs / salvo

A powerful web framework built with a simplified design.
https://salvo.rs
Apache License 2.0
2.92k stars 172 forks source link

stack overflow when using recursive struct with openapi #777

Closed msmaiaa closed 1 month ago

msmaiaa commented 1 month ago

Describe the bug When using recursive structs with the open api router the program outputs this error: "thread 'main' has overflowed its stack".

To Reproduce Steps to reproduce the behavior:

#[derive(ToSchema, serde::Serialize)]
struct MyFirstStruct {
    field: MySecondStruct,
}

#[derive(ToSchema, serde::Serialize)]
struct MySecondStruct {
    field: Option<Box<MyFirstStruct>>,
}

#[endpoint]
async fn hello_route() -> Json<MyFirstStruct> {
    Json(MyFirstStruct {
        field: MySecondStruct { field: None },
    })
}

#[tokio::main]
async fn main() {
    let router = Router::new().get(hello_route);
    let doc = OpenApi::new("game api", "0.0.1").merge_router(&router);

    let router = router
        .push(doc.into_router("/api-doc/openapi.json"))
        .push(SwaggerUi::new("/api-doc/openapi.json").into_router("swagger-ui"));
    let acceptor = TcpListener::new("0.0.0.0:5800")
        .bind()
        .await;
    Server::new(acceptor).serve(router).await;
}

Expected behavior I expected to not crash

Desktop (please complete the following information):

Additional context The program runs just fine if i use the "handler" macro instead of "endpoint".

TheAwiteb commented 1 month ago

The reason is because you have MySecondStruct in MyFirstStruct and MyFirstStruct in MySecondStruct, and as you can see when we expand the macros, the ToSchema::to_schema will call ToSchema::to_schema of its fields (Our fields is in each other = infinity call)

@chrislearn May this debug helps you, or you can explain to me how to fix it, I'll fix it if I can.

ToSchema::to_schema implantation (by the `ToSchema` derive macro) ```rust impl salvo::oapi::ToSchema for MyFirstStruct { fn to_schema( components: &mut salvo::oapi::Components, ) -> salvo::oapi::RefOr { let schema = salvo::oapi::Object::new() .property( "field", if std::any::TypeId::of::() == std::any::TypeId::of::() { salvo::oapi::RefOr::< salvo::oapi::Schema, >::Ref(salvo::oapi::schema::Ref::new("#")) } else { // The problem is here ::to_schema(components) }, ) .required("field"); components .schemas .insert( std::any::type_name::().replace("::", "."), schema.into(), ); salvo::oapi::RefOr::Ref( salvo::oapi::Ref::new({ let res = { let res = ::alloc::fmt::format( format_args!( "#/components/schemas/{0}", std::any::type_name::< MyFirstStruct > ().replace("::", ".") ), ); res }; res }), ) } } impl salvo::oapi::ToSchema for MySecondStruct { fn to_schema( components: &mut salvo::oapi::Components, ) -> salvo::oapi::RefOr { let schema = salvo::oapi::Object::new() .property( "field", salvo::oapi::schema::AllOf::new() .nullable(true) .item( if std::any::TypeId::of::() == std::any::TypeId::of::() { salvo::oapi::RefOr::< salvo::oapi::Schema, >::Ref(salvo::oapi::schema::Ref::new("#")) } else { // The problem is here ::to_schema( components, ) }, ), ); components .schemas .insert( std::any::type_name::().replace("::", "."), schema.into(), ); salvo::oapi::RefOr::Ref( salvo::oapi::Ref::new({ let res = { let res = ::alloc::fmt::format( format_args!( "#/components/schemas/{0}", std::any::type_name::< MySecondStruct > ().replace("::", ".") ), ); res }; res }), ) } } ```
OpenAPI output (if this issue fixed) ```json { "openapi": "3.1.0", "info": { "title": "game api", "version": "0.0.1" }, "paths": { "/": { "get": { "operationId": "test_code.hello_route", "responses": { "200": { "description": "Response with json format data", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/test_code.MyFirstStruct" } } } } } } } }, "components": { "schemas": { "test_code.MyFirstStruct": { "type": "object", "required": [ "field" ], "properties": { "field": { "$ref": "#/components/schemas/test_code.MySecondStruct" } } }, "test_code.MySecondStruct": { "type": "object", "properties": { "field": { "allOf": [ { "$ref": "#/components/schemas/test_code.MyFirstStruct" } ], "nullable": true } } } } } } ```
chrislearn commented 1 month ago

@TheAwiteb Thanks, I'm refactoring openapi and this bug will be fixed in the next few days.