Skip to content

Commit c38f1c5

Browse files
curtiscookcdariuszkuc
authored
fix: move exception handling into ktor statuspages (#1936)
### 📝 Description Currently, the Ktor version of the GraphQL server swallows a base exception, which disallows any custom error responses. This PR moves exception handling to [statuspages](https://ktor.io/docs/status-pages.html), which is the recommended error handler. Note: This is a breaking change for people relying on the current functionality to handle errors ### 🔗 Related Issues #1920 --------- Co-authored-by: c <[email protected]> Co-authored-by: Dariusz Kuc <[email protected]>
1 parent e8b2265 commit c38f1c5

File tree

9 files changed

+75
-9
lines changed

9 files changed

+75
-9
lines changed

examples/server/ktor-server/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ dependencies {
1515
implementation(libs.ktor.server.netty)
1616
implementation(libs.ktor.server.websockets)
1717
implementation(libs.ktor.server.cors)
18+
implementation(libs.ktor.server.statuspages)
1819
implementation(libs.logback)
1920
implementation(libs.kotlinx.coroutines.jdk8)
2021
}

examples/server/ktor-server/src/main/kotlin/com/expediagroup/graphql/examples/server/ktor/GraphQLModule.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import com.expediagroup.graphql.examples.server.ktor.schema.dataloaders.BookData
2626
import com.expediagroup.graphql.examples.server.ktor.schema.dataloaders.CourseDataLoader
2727
import com.expediagroup.graphql.examples.server.ktor.schema.dataloaders.UniversityDataLoader
2828
import com.expediagroup.graphql.server.ktor.GraphQL
29+
import com.expediagroup.graphql.server.ktor.defaultGraphQLStatusPages
2930
import com.expediagroup.graphql.server.ktor.graphQLGetRoute
3031
import com.expediagroup.graphql.server.ktor.graphQLPostRoute
3132
import com.expediagroup.graphql.server.ktor.graphQLSDLRoute
@@ -35,6 +36,7 @@ import io.ktor.serialization.jackson.JacksonWebsocketContentConverter
3536
import io.ktor.server.application.Application
3637
import io.ktor.server.application.install
3738
import io.ktor.server.plugins.cors.routing.CORS
39+
import io.ktor.server.plugins.statuspages.StatusPages
3840
import io.ktor.server.routing.Routing
3941
import io.ktor.server.websocket.WebSockets
4042
import io.ktor.server.websocket.pingPeriod
@@ -45,6 +47,9 @@ fun Application.graphQLModule() {
4547
pingPeriod = Duration.ofSeconds(1)
4648
contentConverter = JacksonWebsocketContentConverter()
4749
}
50+
install(StatusPages) {
51+
defaultGraphQLStatusPages()
52+
}
4853
install(CORS) {
4954
anyHost()
5055
}

gradle/libs.versions.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,10 @@ ktor-client-serialization = { group = "io.ktor", name = "ktor-client-serializati
7676
ktor-client-websockets = { group = "io.ktor", name = "ktor-client-websockets", version.ref = "ktor" }
7777
ktor-serialization-jackson = { group = "io.ktor", name = "ktor-serialization-jackson", version.ref = "ktor" }
7878
ktor-server-core = { group = "io.ktor", name = "ktor-server-core", version.ref = "ktor" }
79+
ktor-server-cors = { group = "io.ktor", name = "ktor-server-cors", version.ref = "ktor" }
7980
ktor-server-content = { group = "io.ktor", name = "ktor-server-content-negotiation", version.ref = "ktor" }
81+
ktor-server-statuspages = { group = "io.ktor", name = "ktor-server-status-pages", version.ref = "ktor" }
8082
ktor-server-websockets = { group = "io.ktor", name = "ktor-server-websockets", version.ref = "ktor" }
81-
ktor-server-cors = { group = "io.ktor", name = "ktor-server-cors", version.ref = "ktor" }
8283
maven-plugin-annotations = { group = "org.apache.maven.plugin-tools", name = "maven-plugin-annotations", version.ref = "maven-plugin-annotation" }
8384
maven-plugin-api = { group = "org.apache.maven", name = "maven-plugin-api", version.ref = "maven-plugin-api" }
8485
maven-project = { group = "org.apache.maven", name = "maven-project", version.ref = "maven-project" }

servers/graphql-kotlin-ktor-server/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ dependencies {
1111
api(libs.ktor.server.core)
1212
api(libs.ktor.server.content)
1313
api(libs.ktor.server.websockets)
14+
api(libs.ktor.server.statuspages)
1415
testImplementation(libs.kotlinx.coroutines.test)
1516
testImplementation(libs.ktor.client.content)
1617
testImplementation(libs.ktor.client.websockets)

servers/graphql-kotlin-ktor-server/src/main/kotlin/com/expediagroup/graphql/server/ktor/GraphQL.kt

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -189,12 +189,7 @@ internal fun List<Any>.toTopLevelObjects(): List<TopLevelObject> = this.map {
189189
TopLevelObject(it)
190190
}
191191

192-
internal suspend inline fun KtorGraphQLServer.executeRequest(call: ApplicationCall) = try {
192+
internal suspend inline fun KtorGraphQLServer.executeRequest(call: ApplicationCall) =
193193
execute(call.request)?.let {
194194
call.respond(it)
195195
} ?: call.respond(HttpStatusCode.BadRequest)
196-
} catch (e: UnsupportedOperationException) {
197-
call.respond(HttpStatusCode.MethodNotAllowed)
198-
} catch (e: Exception) {
199-
call.respond(HttpStatusCode.BadRequest)
200-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2024 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.server.ktor
18+
19+
import io.ktor.http.HttpStatusCode
20+
import io.ktor.server.plugins.statuspages.StatusPagesConfig
21+
import io.ktor.server.response.respond
22+
23+
/**
24+
* Configures default exception handling using Ktor Status Pages.
25+
*
26+
* Returns following HTTP status codes:
27+
* * 405 (Method Not Allowed) - when attempting to execute mutation or query through a GET request
28+
* * 400 (Bad Request) - any other exception
29+
*/
30+
fun StatusPagesConfig.defaultGraphQLStatusPages(): StatusPagesConfig {
31+
exception<Throwable> { call, cause ->
32+
when (cause) {
33+
is UnsupportedOperationException -> call.respond(HttpStatusCode.MethodNotAllowed)
34+
else -> call.respond(HttpStatusCode.BadRequest)
35+
}
36+
}
37+
return this
38+
}

servers/graphql-kotlin-ktor-server/src/test/kotlin/com/expediagroup/graphql/server/ktor/GraphQLPluginTest.kt

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import io.ktor.http.contentType
3636
import io.ktor.serialization.jackson.jackson
3737
import io.ktor.server.application.Application
3838
import io.ktor.server.application.install
39+
import io.ktor.server.plugins.statuspages.StatusPages
3940
import io.ktor.server.routing.Routing
4041
import io.ktor.server.testing.testApplication
4142
import io.ktor.websocket.Frame
@@ -183,13 +184,23 @@ class GraphQLPluginTest {
183184
}
184185

185186
@Test
186-
fun `server should return Bad Request for invalid POST requests`() {
187+
fun `server should return Bad Request for invalid POST requests with correct content type`() {
187188
testApplication {
188-
val response = client.post("/graphql")
189+
val response = client.post("/graphql") {
190+
contentType(ContentType.Application.Json)
191+
}
189192
assertEquals(HttpStatusCode.BadRequest, response.status)
190193
}
191194
}
192195

196+
@Test
197+
fun `server should return Unsupported Media Type for POST requests with invalid content type`() {
198+
testApplication {
199+
val response = client.post("/graphql")
200+
assertEquals(HttpStatusCode.UnsupportedMediaType, response.status)
201+
}
202+
}
203+
193204
@Test
194205
fun `server should handle subscription requests`() {
195206
testApplication {
@@ -234,6 +245,9 @@ class GraphQLPluginTest {
234245
}
235246

236247
fun Application.testGraphQLModule() {
248+
install(StatusPages) {
249+
defaultGraphQLStatusPages()
250+
}
237251
install(GraphQL) {
238252
schema {
239253
// packages property is read from application.conf

website/docs/server/ktor-server/ktor-http-request-response.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ fun Application.myModule() {
2424
// install additional plugins
2525
install(CORS) { ... }
2626
install(Authentication) { ... }
27+
install(StatusPages) { ... }
2728

2829
// install graphql plugin
2930
install(GraphQL) {

website/docs/server/ktor-server/ktor-overview.mdx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ fun Application.graphQLModule() {
6666
install(Routing) {
6767
graphQLPostRoute()
6868
}
69+
install(StatusPages) {
70+
defaultGraphQLStatusPages()
71+
}
6972
}
7073
```
7174

@@ -99,6 +102,13 @@ GraphQL plugin provides following `Route` extension functions
99102
- `Route#graphQLSDLRoute` - GraphQL route for exposing schema in Schema Definition Language (SDL) format
100103
- `Route#graphiQLRoute` - GraphQL route for exposing [an official IDE](https://github.com/graphql/graphiql) from the GraphQL Foundation
101104

105+
## StatusPages
106+
107+
`graphql-kotlin-ktor-server` plugin differs from Spring as it relies on Ktor's StatusPages plugin to perform error handling.
108+
It is recommended to use the default settings, however, if you would like to customize your error handling you can create
109+
your own handler. One example might be if you need to catch a custom Authorization error to return a 401 status code.
110+
Please see [Ktor's Official Documentation for StatusPages](https://ktor.io/docs/server-status-pages.html)
111+
102112
## GraalVm Native Image Support
103113

104114
GraphQL Kotlin Ktor Server can be compiled to a [native image](https://www.graalvm.org/latest/reference-manual/native-image/)

0 commit comments

Comments
 (0)