Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: move exception handling into ktor statuspages #1936

Merged
merged 9 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/server/ktor-server/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,17 @@ import io.ktor.server.plugins.cors.routing.CORS
import io.ktor.server.routing.Routing
import io.ktor.server.websocket.WebSockets
import io.ktor.server.websocket.pingPeriod
import java.lang.UnsupportedOperationException
import java.time.Duration

fun Application.graphQLModule() {
install(WebSockets) {
pingPeriod = Duration.ofSeconds(1)
contentConverter = JacksonWebsocketContentConverter()
}
install(StatusPages) {
defaultGraphQLStatusPages()
}
install(CORS) {
anyHost()
}
Expand Down
3 changes: 2 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,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" }
Expand Down
1 change: 1 addition & 0 deletions servers/graphql-kotlin-ktor-server/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,12 +189,7 @@ internal fun List<Any>.toTopLevelObjects(): List<TopLevelObject> = 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)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.expediagroup.graphql.server.ktor

import io.ktor.http.HttpStatusCode
import io.ktor.server.plugins.statuspages.StatusPagesConfig
import io.ktor.server.response.respond

fun StatusPagesConfig.defaultGraphQLStatusPages(): StatusPagesConfig {
exception<Throwable> { call, cause ->
when (cause) {
is UnsupportedOperationException -> call.respond(HttpStatusCode.MethodNotAllowed)
else -> call.respond(HttpStatusCode.BadRequest) } }
return this
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ 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.response.respond
import io.ktor.server.routing.Routing
import io.ktor.server.testing.testApplication
import io.ktor.websocket.Frame
Expand Down Expand Up @@ -180,13 +182,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 {
Expand Down Expand Up @@ -231,6 +243,9 @@ class GraphQLPluginTest {
}

fun Application.testGraphQLModule() {
install(StatusPages) {
defaultGraphQLStatusPages()
}
install(GraphQL) {
schema {
// packages property is read from application.conf
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ fun Application.myModule() {
// install additional plugins
install(CORS) { ... }
install(Authentication) { ... }
install(StatusPages) { ... }

// install graphql plugin
install(GraphQL) {
Expand Down
7 changes: 7 additions & 0 deletions website/docs/server/ktor-server/ktor-overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,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/)
Expand Down