diff --git a/examples/server/ktor-server/build.gradle.kts b/examples/server/ktor-server/build.gradle.kts index 5584d65c39..fde64825c2 100644 --- a/examples/server/ktor-server/build.gradle.kts +++ b/examples/server/ktor-server/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { implementation(libs.ktor.server.netty) implementation(libs.ktor.server.websockets) implementation(libs.ktor.server.cors) + implementation(libs.ktor.server.statuspages) implementation(libs.logback) implementation(libs.kotlinx.coroutines.jdk8) } diff --git a/examples/server/ktor-server/src/main/kotlin/com/expediagroup/graphql/examples/server/ktor/GraphQLModule.kt b/examples/server/ktor-server/src/main/kotlin/com/expediagroup/graphql/examples/server/ktor/GraphQLModule.kt index 389e88fbc7..54a456408d 100644 --- a/examples/server/ktor-server/src/main/kotlin/com/expediagroup/graphql/examples/server/ktor/GraphQLModule.kt +++ b/examples/server/ktor-server/src/main/kotlin/com/expediagroup/graphql/examples/server/ktor/GraphQLModule.kt @@ -26,6 +26,7 @@ import com.expediagroup.graphql.examples.server.ktor.schema.dataloaders.BookData import com.expediagroup.graphql.examples.server.ktor.schema.dataloaders.CourseDataLoader import com.expediagroup.graphql.examples.server.ktor.schema.dataloaders.UniversityDataLoader import com.expediagroup.graphql.server.ktor.GraphQL +import com.expediagroup.graphql.server.ktor.defaultGraphQLStatusPages import com.expediagroup.graphql.server.ktor.graphQLGetRoute import com.expediagroup.graphql.server.ktor.graphQLPostRoute import com.expediagroup.graphql.server.ktor.graphQLSDLRoute @@ -35,6 +36,7 @@ import io.ktor.serialization.jackson.JacksonWebsocketContentConverter import io.ktor.server.application.Application import io.ktor.server.application.install import io.ktor.server.plugins.cors.routing.CORS +import io.ktor.server.plugins.statuspages.StatusPages import io.ktor.server.routing.Routing import io.ktor.server.websocket.WebSockets import io.ktor.server.websocket.pingPeriod @@ -45,6 +47,9 @@ fun Application.graphQLModule() { pingPeriod = Duration.ofSeconds(1) contentConverter = JacksonWebsocketContentConverter() } + install(StatusPages) { + defaultGraphQLStatusPages() + } install(CORS) { anyHost() } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 65dfbb199a..5da1198082 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -76,9 +76,10 @@ ktor-client-serialization = { group = "io.ktor", name = "ktor-client-serializati ktor-client-websockets = { group = "io.ktor", name = "ktor-client-websockets", version.ref = "ktor" } ktor-serialization-jackson = { group = "io.ktor", name = "ktor-serialization-jackson", version.ref = "ktor" } ktor-server-core = { group = "io.ktor", name = "ktor-server-core", version.ref = "ktor" } +ktor-server-cors = { group = "io.ktor", name = "ktor-server-cors", version.ref = "ktor" } ktor-server-content = { group = "io.ktor", name = "ktor-server-content-negotiation", version.ref = "ktor" } +ktor-server-statuspages = { group = "io.ktor", name = "ktor-server-status-pages", version.ref = "ktor" } ktor-server-websockets = { group = "io.ktor", name = "ktor-server-websockets", version.ref = "ktor" } -ktor-server-cors = { group = "io.ktor", name = "ktor-server-cors", version.ref = "ktor" } maven-plugin-annotations = { group = "org.apache.maven.plugin-tools", name = "maven-plugin-annotations", version.ref = "maven-plugin-annotation" } maven-plugin-api = { group = "org.apache.maven", name = "maven-plugin-api", version.ref = "maven-plugin-api" } maven-project = { group = "org.apache.maven", name = "maven-project", version.ref = "maven-project" } diff --git a/servers/graphql-kotlin-ktor-server/build.gradle.kts b/servers/graphql-kotlin-ktor-server/build.gradle.kts index ae8f3c8ae0..d4409dfee7 100644 --- a/servers/graphql-kotlin-ktor-server/build.gradle.kts +++ b/servers/graphql-kotlin-ktor-server/build.gradle.kts @@ -11,6 +11,7 @@ dependencies { api(libs.ktor.server.core) api(libs.ktor.server.content) api(libs.ktor.server.websockets) + api(libs.ktor.server.statuspages) testImplementation(libs.kotlinx.coroutines.test) testImplementation(libs.ktor.client.content) testImplementation(libs.ktor.client.websockets) diff --git a/servers/graphql-kotlin-ktor-server/src/main/kotlin/com/expediagroup/graphql/server/ktor/GraphQL.kt b/servers/graphql-kotlin-ktor-server/src/main/kotlin/com/expediagroup/graphql/server/ktor/GraphQL.kt index b9f8f30435..bc6693f7ac 100644 --- a/servers/graphql-kotlin-ktor-server/src/main/kotlin/com/expediagroup/graphql/server/ktor/GraphQL.kt +++ b/servers/graphql-kotlin-ktor-server/src/main/kotlin/com/expediagroup/graphql/server/ktor/GraphQL.kt @@ -189,12 +189,7 @@ internal fun List.toTopLevelObjects(): List = this.map { TopLevelObject(it) } -internal suspend inline fun KtorGraphQLServer.executeRequest(call: ApplicationCall) = try { +internal suspend inline fun KtorGraphQLServer.executeRequest(call: ApplicationCall) = execute(call.request)?.let { call.respond(it) } ?: call.respond(HttpStatusCode.BadRequest) -} catch (e: UnsupportedOperationException) { - call.respond(HttpStatusCode.MethodNotAllowed) -} catch (e: Exception) { - call.respond(HttpStatusCode.BadRequest) -} 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 new file mode 100644 index 0000000000..74ce52b2cb --- /dev/null +++ b/servers/graphql-kotlin-ktor-server/src/main/kotlin/com/expediagroup/graphql/server/ktor/GraphQLStatusPages.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024 Expedia, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expediagroup.graphql.server.ktor + +import io.ktor.http.HttpStatusCode +import io.ktor.server.plugins.statuspages.StatusPagesConfig +import io.ktor.server.response.respond + +/** + * Configures default exception handling using Ktor Status Pages. + * + * Returns following HTTP status codes: + * * 405 (Method Not Allowed) - when attempting to execute mutation or query through a GET request + * * 400 (Bad Request) - any other exception + */ +fun StatusPagesConfig.defaultGraphQLStatusPages(): StatusPagesConfig { + exception { call, cause -> + when (cause) { + is UnsupportedOperationException -> call.respond(HttpStatusCode.MethodNotAllowed) + else -> call.respond(HttpStatusCode.BadRequest) + } + } + return this +} 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 db2d17cfb0..510bb689ae 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,6 +36,7 @@ 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.plugins.statuspages.StatusPages import io.ktor.server.routing.Routing import io.ktor.server.testing.testApplication import io.ktor.websocket.Frame @@ -183,13 +184,23 @@ class GraphQLPluginTest { } @Test - fun `server should return Bad Request for invalid POST requests`() { + fun `server should return Bad Request for invalid POST requests with correct content type`() { testApplication { - val response = client.post("/graphql") + val response = client.post("/graphql") { + contentType(ContentType.Application.Json) + } assertEquals(HttpStatusCode.BadRequest, response.status) } } + @Test + fun `server should return Unsupported Media Type for POST requests with invalid content type`() { + testApplication { + val response = client.post("/graphql") + assertEquals(HttpStatusCode.UnsupportedMediaType, response.status) + } + } + @Test fun `server should handle subscription requests`() { testApplication { @@ -234,6 +245,9 @@ class GraphQLPluginTest { } fun Application.testGraphQLModule() { + install(StatusPages) { + defaultGraphQLStatusPages() + } install(GraphQL) { schema { // packages property is read from application.conf diff --git a/website/docs/server/ktor-server/ktor-http-request-response.md b/website/docs/server/ktor-server/ktor-http-request-response.md index 5054275768..cf1285a21f 100644 --- a/website/docs/server/ktor-server/ktor-http-request-response.md +++ b/website/docs/server/ktor-server/ktor-http-request-response.md @@ -24,6 +24,7 @@ fun Application.myModule() { // install additional plugins install(CORS) { ... } install(Authentication) { ... } + install(StatusPages) { ... } // install graphql plugin install(GraphQL) { diff --git a/website/docs/server/ktor-server/ktor-overview.mdx b/website/docs/server/ktor-server/ktor-overview.mdx index 66798dac42..eda01ae6a3 100644 --- a/website/docs/server/ktor-server/ktor-overview.mdx +++ b/website/docs/server/ktor-server/ktor-overview.mdx @@ -66,6 +66,9 @@ fun Application.graphQLModule() { install(Routing) { graphQLPostRoute() } + install(StatusPages) { + defaultGraphQLStatusPages() + } } ``` @@ -99,6 +102,13 @@ GraphQL plugin provides following `Route` extension functions - `Route#graphQLSDLRoute` - GraphQL route for exposing schema in Schema Definition Language (SDL) format - `Route#graphiQLRoute` - GraphQL route for exposing [an official IDE](https://github.com/graphql/graphiql) from the GraphQL Foundation +## StatusPages + +`graphql-kotlin-ktor-server` plugin differs from Spring as it relies on Ktor's StatusPages plugin to perform error handling. +It is recommended to use the default settings, however, if you would like to customize your error handling you can create +your own handler. One example might be if you need to catch a custom Authorization error to return a 401 status code. +Please see [Ktor's Official Documentation for StatusPages](https://ktor.io/docs/server-status-pages.html) + ## GraalVm Native Image Support GraphQL Kotlin Ktor Server can be compiled to a [native image](https://www.graalvm.org/latest/reference-manual/native-image/)