diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 408935a199..4ef7a1b2f9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ kotlinx-benchmark = "0.4.11" kotlinx-coroutines = "1.8.1" # TODO kotlin 1.9 upgrade: fix GraphQLTestUtils and GenerateKotlinxClientIT kotlinx-serialization = "1.6.3" -ktor = "2.3.12" +ktor = "3.0.1" fastjson2 = "2.0.53" maven-plugin-annotation = "3.13.1" maven-plugin-api = "3.9.8" diff --git a/servers/graphql-kotlin-ktor-server/src/main/kotlin/com/expediagroup/graphql/server/ktor/GraphQLRoutes.kt b/servers/graphql-kotlin-ktor-server/src/main/kotlin/com/expediagroup/graphql/server/ktor/GraphQLRoutes.kt index 7ec6e232f2..4ec2ea2ab2 100644 --- a/servers/graphql-kotlin-ktor-server/src/main/kotlin/com/expediagroup/graphql/server/ktor/GraphQLRoutes.kt +++ b/servers/graphql-kotlin-ktor-server/src/main/kotlin/com/expediagroup/graphql/server/ktor/GraphQLRoutes.kt @@ -21,8 +21,6 @@ import com.expediagroup.graphql.server.execution.subscription.GRAPHQL_WS_PROTOCO import com.fasterxml.jackson.databind.ObjectMapper import io.ktor.http.ContentType import io.ktor.serialization.jackson.jackson -import io.ktor.server.application.call -import io.ktor.server.application.install import io.ktor.server.application.plugin import io.ktor.server.plugins.contentnegotiation.ContentNegotiation import io.ktor.server.response.respondText @@ -115,11 +113,11 @@ fun Route.graphiQLRoute( graphQLEndpoint: String = "graphql", subscriptionsEndpoint: String = "subscriptions", ): Route { - val contextPath = this.environment?.rootPath + val contextPath = this.application.rootPath val graphiQL = GraphQL::class.java.classLoader.getResourceAsStream("graphql-graphiql.html")?.bufferedReader()?.use { reader -> reader.readText() - .replace("\${graphQLEndpoint}", if (contextPath.isNullOrBlank()) graphQLEndpoint else "$contextPath/$graphQLEndpoint") - .replace("\${subscriptionsEndpoint}", if (contextPath.isNullOrBlank()) subscriptionsEndpoint else "$contextPath/$subscriptionsEndpoint") + .replace("\${graphQLEndpoint}", if (contextPath.isBlank()) graphQLEndpoint else "$contextPath/$graphQLEndpoint") + .replace("\${subscriptionsEndpoint}", if (contextPath.isBlank()) subscriptionsEndpoint else "$contextPath/$subscriptionsEndpoint") } ?: throw IllegalStateException("Unable to load GraphiQL") return get(endpoint) { call.respondText(graphiQL, ContentType.Text.Html) diff --git a/servers/graphql-kotlin-ktor-server/src/main/kotlin/com/expediagroup/graphql/server/ktor/GraphQLStatusPages.kt b/servers/graphql-kotlin-ktor-server/src/main/kotlin/com/expediagroup/graphql/server/ktor/GraphQLStatusPages.kt index 74ce52b2cb..2363eb09d7 100644 --- a/servers/graphql-kotlin-ktor-server/src/main/kotlin/com/expediagroup/graphql/server/ktor/GraphQLStatusPages.kt +++ b/servers/graphql-kotlin-ktor-server/src/main/kotlin/com/expediagroup/graphql/server/ktor/GraphQLStatusPages.kt @@ -31,6 +31,7 @@ fun StatusPagesConfig.defaultGraphQLStatusPages(): StatusPagesConfig { exception { call, cause -> when (cause) { is UnsupportedOperationException -> call.respond(HttpStatusCode.MethodNotAllowed) + is InvalidPayloadException -> call.respond(HttpStatusCode.UnsupportedMediaType) else -> call.respond(HttpStatusCode.BadRequest) } } diff --git a/servers/graphql-kotlin-ktor-server/src/main/kotlin/com/expediagroup/graphql/server/ktor/KtorGraphQLRequestParser.kt b/servers/graphql-kotlin-ktor-server/src/main/kotlin/com/expediagroup/graphql/server/ktor/KtorGraphQLRequestParser.kt index 006889e11d..8b66b98d93 100644 --- a/servers/graphql-kotlin-ktor-server/src/main/kotlin/com/expediagroup/graphql/server/ktor/KtorGraphQLRequestParser.kt +++ b/servers/graphql-kotlin-ktor-server/src/main/kotlin/com/expediagroup/graphql/server/ktor/KtorGraphQLRequestParser.kt @@ -31,6 +31,8 @@ internal const val REQUEST_PARAM_QUERY = "query" internal const val REQUEST_PARAM_OPERATION_NAME = "operationName" internal const val REQUEST_PARAM_VARIABLES = "variables" +class InvalidPayloadException(message: String, throwable: Throwable) : IllegalStateException(message, throwable) + /** * GraphQL Ktor [ApplicationRequest] parser. */ @@ -62,6 +64,6 @@ open class KtorGraphQLRequestParser( private suspend fun parsePostRequest(request: ApplicationRequest): GraphQLServerRequest? = try { request.call.receive() } catch (e: IOException) { - throw IllegalStateException("Invalid HTTP request - unable to parse GraphQL request from POST payload", e) + throw InvalidPayloadException("Invalid HTTP request - unable to parse GraphQL request from POST payload", e) } } diff --git a/servers/graphql-kotlin-ktor-server/src/test/kotlin/com/expediagroup/graphql/server/ktor/GraphQLPluginTest.kt b/servers/graphql-kotlin-ktor-server/src/test/kotlin/com/expediagroup/graphql/server/ktor/GraphQLPluginTest.kt index 510bb689ae..492b2442b3 100644 --- a/servers/graphql-kotlin-ktor-server/src/test/kotlin/com/expediagroup/graphql/server/ktor/GraphQLPluginTest.kt +++ b/servers/graphql-kotlin-ktor-server/src/test/kotlin/com/expediagroup/graphql/server/ktor/GraphQLPluginTest.kt @@ -36,8 +36,9 @@ import io.ktor.http.contentType import io.ktor.serialization.jackson.jackson import io.ktor.server.application.Application import io.ktor.server.application.install +import io.ktor.server.config.* import io.ktor.server.plugins.statuspages.StatusPages -import io.ktor.server.routing.Routing +import io.ktor.server.routing.* import io.ktor.server.testing.testApplication import io.ktor.websocket.Frame import io.ktor.websocket.readText @@ -104,7 +105,7 @@ class GraphQLPluginTest { flow: Int! } """.trimIndent() - testApplication { + testModule { val response = client.get("/sdl") assertEquals(HttpStatusCode.OK, response.status) assertEquals(expectedSchema, response.bodyAsText().trim()) @@ -113,7 +114,7 @@ class GraphQLPluginTest { @Test fun `server should handle valid GET requests`() { - testApplication { + testModule { val response = client.get("/graphql") { parameter("query", "query HelloQuery(\$name: String){ hello(name: \$name) }") parameter("operationName", "HelloQuery") @@ -126,7 +127,7 @@ class GraphQLPluginTest { @Test fun `server should return Method Not Allowed for Mutation GET requests`() { - testApplication { + testModule { val response = client.get("/graphql") { parameter("query", "mutation { foo }") } @@ -136,7 +137,7 @@ class GraphQLPluginTest { @Test fun `server should return Bad Request for invalid GET requests`() { - testApplication { + testModule { val response = client.get("/graphql") assertEquals(HttpStatusCode.BadRequest, response.status) } @@ -144,7 +145,7 @@ class GraphQLPluginTest { @Test fun `server should handle valid POST requests`() { - testApplication { + testModule { val client = createClient { install(ContentNegotiation) { jackson() @@ -161,7 +162,7 @@ class GraphQLPluginTest { @Test fun `server should handle valid POST batch requests`() { - testApplication { + testModule { val client = createClient { install(ContentNegotiation) { jackson() @@ -185,7 +186,7 @@ class GraphQLPluginTest { @Test fun `server should return Bad Request for invalid POST requests with correct content type`() { - testApplication { + testModule { val response = client.post("/graphql") { contentType(ContentType.Application.Json) } @@ -195,7 +196,7 @@ class GraphQLPluginTest { @Test fun `server should return Unsupported Media Type for POST requests with invalid content type`() { - testApplication { + testModule { val response = client.post("/graphql") assertEquals(HttpStatusCode.UnsupportedMediaType, response.status) } @@ -203,7 +204,7 @@ class GraphQLPluginTest { @Test fun `server should handle subscription requests`() { - testApplication { + testModule { val client = createClient { install(ContentNegotiation) { jackson() @@ -233,7 +234,7 @@ class GraphQLPluginTest { @Test fun `server should provide GraphiQL endpoint`() { - testApplication { + testModule { val response = client.get("/graphiql") assertEquals(HttpStatusCode.OK, response.status) @@ -260,7 +261,7 @@ fun Application.testGraphQLModule() { } } install(io.ktor.server.websocket.WebSockets) - install(Routing) { + routing { graphQLGetRoute() graphQLPostRoute() graphQLSubscriptionsRoute() @@ -268,3 +269,11 @@ fun Application.testGraphQLModule() { graphiQLRoute() } } + +private fun testModule(block: suspend io.ktor.server.testing.ApplicationTestBuilder.() -> kotlin.Unit) = testApplication { + environment { + config = ApplicationConfig(("application.conf")) + } + block() +} +