SMILEY4 / ktor-swagger-ui

Kotlin Ktor plugin to generate OpenAPI and provide Swagger UI
Apache License 2.0
182 stars 33 forks source link

Not Acceptable error occurs when the response has multiple response media types. #69

Closed x2d7751347m closed 11 months ago

x2d7751347m commented 1 year ago

Hello!

Not Acceptable error occurs when the response has multiple response media types. In my case, text-plain and application.json.

1

here is the flow.

post({
                response {
                    HttpStatusCode.Created to {
                        description = "Created"
                        body<String> {
                        }
                    }
                    HttpStatusCode.BadRequest to {
                        description = "Bad Request"
                        body<SerializableClass> {
                        }
                    }
                }
            }) {
             if(bluh) {
             call.respond(SerializableClass(message = "hello"))
             } else {
             call.respond("String")
             }
    }
SMILEY4 commented 1 year ago

Hi, i'm having issues reproducing the problem. My attempt currently looks like this:

import io.github.smiley4.ktorswaggerui.SwaggerUI
import io.github.smiley4.ktorswaggerui.dsl.post
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.Application
import io.ktor.server.application.call
import io.ktor.server.application.install
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import io.ktor.server.response.respond
import io.ktor.server.routing.routing

fun main() {
    embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true)
}

private fun Application.myModule() {

    data class MyClass(
        val id: Int,
        val name: String,
        val tag: String
    )

    install(SwaggerUI)

    routing {
        post({
            response {
                HttpStatusCode.Created to {   // text/plain
                    description = "Created"
                    body<String>()
                }
                HttpStatusCode.BadRequest to {  // application/json
                    description = "Bad Request"
                    body<MyClass>()
                }
            }
        }) {
            call.respond("Hello")
        }
    }
}

... though this does not throw any errors.

Could you maybe post a fully runnable example showcasing the error ?

x2d7751347m commented 1 year ago
routing {
        post({
                request {
                    queryParameter<Boolean>("bool") {
                        required = true
                    }
                }
            response {
                HttpStatusCode.Created to {   // text/plain
                    description = "Created"
                    body<String>()
                }
                HttpStatusCode.BadRequest to {  // application/json
                    description = "Bad Request"
                    body<MyClass>()
                }
            }
        }) {
        val bool = call.parameters["bool"].toBoolean()
           //It works
            if(bool) call.respond("Hello")
            // It occurs 406
           else call.respond(MyClass(1, "name", "tag"))
        }
    }
SMILEY4 commented 1 year ago

Thank you. This does not seem like a problem with the swagger-ui plugin. Can you check if you have the ContentNegotiation plugin installed and configured and add if it not? You can find more information here: https://ktor.io/docs/serialization.html.

x2d7751347m commented 1 year ago

Sure. This is my plugin settings.

install(ContentNegotiation) {
        json(
            Json {
                prettyPrint = true
                isLenient = true
                encodeDefaults = true
            }
        )
    }
x2d7751347m commented 1 year ago

I hope this may help you.

The accept header of the request is causing this problem. It is fixed to the first response type in Swagger.

// First Example
routing {
        post("test", {
            request {
                queryParameter<Boolean>("bool") {
                    required = true
                }
            }

           // It works with json
            response {
                HttpStatusCode.Created to {   // application/json
                    description = "Created"
                    body<MyClass>()
                }
                HttpStatusCode.BadRequest to {  // text/plain
                    description = "Bad Request"
                    body<String>()
                }
            }

        }) {
            val bool = call.parameters["bool"].toBoolean()
            if (bool) call.respond("Hello")
            else call.respond(MyClass(1, "name", "tag"))
        }
    }

In swagger Accept header of this curl request is application/json, And the content type of the response is text/plain. But it works. image And of course, It works. image

// Second Example
routing {
         //  Separate path from first example
        post("test2", {
            request {
                queryParameter<Boolean>("bool") {
                    required = true
                }
            }
           // Here is the difference.
           // It does not work with json response
            response {
                HttpStatusCode.Created to {   // text/plain
                    description = "Created"
                    body<String>()
                }
                HttpStatusCode.BadRequest to {  // application/json
                    description = "Bad Request"
                    body<MyClass>()
                }
            }
          // Below is the same as the first example.
        }) {
            val bool = call.parameters["bool"].toBoolean()
            if (bool) call.respond("Hello")
            else call.respond(MyClass(1, "name", "tag"))
        }
    }

In swagger image

And with this second example

SMILEY4 commented 1 year ago

Thanks, i understand the issue now. I think swagger chooses the accept header of the 2xx response to send with the request. So this

response {
    HttpStatusCode.BadRequest to {
        description = "Bad Request"
        body<String>()
    }
    HttpStatusCode.Created to { // 2xx http code -> application/json
        description = "Created"
        body<MyClass>()
    }
}

technically works aswell, independent of the order.

I'll look into it more.

x2d7751347m commented 1 year ago

image Setting multiple types in response to the Accept header is the best solution. but I don't know if Swagger can do it.

SMILEY4 commented 11 months ago

Hi, sorry for the late response. As far as i know, swagger automatically chooses the Accept-Header based on the defined responses (i think always one of the 2xx responses). Unfortunately, i dont think this behaviour can be changed or the header overwritten - so there isn't much i can do here. I'd be interested to know however, how other people solved/overcame this issue/limitation.