Skip to content

Commit 3e2fcbb

Browse files
authored
[client] Throw error on non-successful HTTP responses (#889)
Currently GraphQLWebClient throws NoSuchElementException for non-200 with empty body response (i.e 401 Unauthorized) instead of propagating the error. This change makes use of WebClient.retrieve to throw exception on non-sucessful HTTP responses to keep inline with KtorClient. GraphQL always uses HTTP status 200 so other status like 4xx or 5xx should be treated as failure and it does not need to unmarshal response body.
1 parent bb1cfde commit 3e2fcbb

File tree

3 files changed

+72
-4
lines changed

3 files changed

+72
-4
lines changed

clients/graphql-kotlin-ktor-client/src/test/kotlin/com/expediagroup/graphql/client/GraphQLKtorClientTest.kt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,21 @@ package com.expediagroup.graphql.client
33
import com.expediagroup.graphql.types.GraphQLError
44
import com.expediagroup.graphql.types.GraphQLResponse
55
import com.expediagroup.graphql.types.SourceLocation
6+
import com.fasterxml.jackson.databind.exc.MismatchedInputException
67
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
78
import com.github.tomakehurst.wiremock.WireMockServer
89
import com.github.tomakehurst.wiremock.client.MappingBuilder
910
import com.github.tomakehurst.wiremock.client.WireMock
1011
import com.github.tomakehurst.wiremock.core.WireMockConfiguration
1112
import com.github.tomakehurst.wiremock.matching.EqualToPattern
1213
import io.ktor.client.engine.okhttp.OkHttp
14+
import io.ktor.client.features.ServerResponseException
1315
import io.ktor.client.features.logging.DEFAULT
1416
import io.ktor.client.features.logging.LogLevel
1517
import io.ktor.client.features.logging.Logger
1618
import io.ktor.client.features.logging.Logging
1719
import io.ktor.client.request.header
20+
import io.ktor.client.statement.readText
1821
import io.ktor.network.sockets.SocketTimeoutException
1922
import kotlinx.coroutines.runBlocking
2023
import org.junit.jupiter.api.AfterAll
@@ -136,6 +139,38 @@ class GraphQLKtorClientTest {
136139
}
137140
}
138141

142+
@Test
143+
fun `verifies Non-OK HTTP responses will throw error`() {
144+
WireMock.stubFor(
145+
WireMock.post("/graphql")
146+
.willReturn(WireMock.aResponse().withStatus(500).withBody("Internal server error"))
147+
)
148+
149+
val client = GraphQLKtorClient(URL("${wireMockServer.baseUrl()}/graphql"))
150+
val error = assertFailsWith<ServerResponseException> {
151+
runBlocking {
152+
client.execute<HelloWorldResult>("query HelloWorldQuery { helloWorld }")
153+
}
154+
}
155+
assertEquals(500, error.response.status.value)
156+
assertEquals("Internal server error", runBlocking { error.response.readText() })
157+
}
158+
159+
@Test
160+
fun `verifies response with empty body will throw error`() {
161+
WireMock.stubFor(
162+
WireMock.post("/graphql")
163+
.willReturn(WireMock.aResponse().withStatus(204))
164+
)
165+
166+
val client = GraphQLKtorClient(URL("${wireMockServer.baseUrl()}/graphql"))
167+
assertFailsWith<MismatchedInputException> {
168+
runBlocking {
169+
client.execute<HelloWorldResult>("query HelloWorldQuery { helloWorld }")
170+
}
171+
}
172+
}
173+
139174
private fun stubResponse(response: GraphQLResponse<*>, delayMillis: Int = 0): MappingBuilder =
140175
WireMock.post("/graphql")
141176
.willReturn(

clients/graphql-kotlin-spring-client/src/main/kotlin/com/expediagroup/graphql/client/GraphQLWebClient.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,12 @@ import com.fasterxml.jackson.databind.DeserializationFeature
2121
import com.fasterxml.jackson.databind.JavaType
2222
import com.fasterxml.jackson.databind.ObjectMapper
2323
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
24+
import kotlinx.coroutines.reactive.awaitSingle
2425
import org.springframework.http.MediaType
2526
import org.springframework.http.codec.json.Jackson2JsonDecoder
2627
import org.springframework.http.codec.json.Jackson2JsonEncoder
2728
import org.springframework.web.reactive.function.client.ExchangeStrategies
2829
import org.springframework.web.reactive.function.client.WebClient
29-
import org.springframework.web.reactive.function.client.awaitBody
30-
import org.springframework.web.reactive.function.client.awaitExchange
3130

3231
/**
3332
* A lightweight typesafe GraphQL HTTP client using Spring WebClient engine.
@@ -81,8 +80,9 @@ open class GraphQLWebClient(
8180
.accept(MediaType.APPLICATION_JSON)
8281
.contentType(MediaType.APPLICATION_JSON)
8382
.bodyValue(graphQLRequest)
84-
.awaitExchange()
85-
.awaitBody<String>()
83+
.retrieve()
84+
.bodyToMono(String::class.java)
85+
.awaitSingle()
8686

8787
@Suppress("BlockingMethodInNonBlockingContext")
8888
return mapper.readValue(rawResult, parameterizedType(resultType))

clients/graphql-kotlin-spring-client/src/test/kotlin/com/expediagroup/graphql/client/GraphQLWebClientTest.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test
2121
import org.springframework.http.client.reactive.ClientHttpConnector
2222
import org.springframework.http.client.reactive.ReactorClientHttpConnector
2323
import org.springframework.web.reactive.function.client.WebClient
24+
import org.springframework.web.reactive.function.client.WebClientResponseException
2425
import reactor.netty.http.client.HttpClient
2526
import java.util.concurrent.TimeUnit
2627
import kotlin.test.assertEquals
@@ -135,6 +136,38 @@ class GraphQLWebClientTest {
135136
}
136137
}
137138

139+
@Test
140+
fun `verifies Non-OK HTTP responses will throw error`() {
141+
WireMock.stubFor(
142+
WireMock.post("/graphql")
143+
.willReturn(WireMock.aResponse().withStatus(500).withBody("Internal server error"))
144+
)
145+
146+
val client = GraphQLWebClient(url = "${wireMockServer.baseUrl()}/graphql")
147+
val error = assertFailsWith<WebClientResponseException> {
148+
runBlocking {
149+
client.execute<HelloWorldResult>("query HelloWorldQuery { helloWorld }")
150+
}
151+
}
152+
assertEquals(500, error.rawStatusCode)
153+
assertEquals("Internal server error", error.responseBodyAsString)
154+
}
155+
156+
@Test
157+
fun `verifies response with empty body will throw error`() {
158+
WireMock.stubFor(
159+
WireMock.post("/graphql")
160+
.willReturn(WireMock.aResponse().withStatus(204))
161+
)
162+
163+
val client = GraphQLWebClient(url = "${wireMockServer.baseUrl()}/graphql")
164+
assertFailsWith<NoSuchElementException> {
165+
runBlocking {
166+
client.execute<HelloWorldResult>("query HelloWorldQuery { helloWorld }")
167+
}
168+
}
169+
}
170+
138171
private fun stubResponse(response: GraphQLResponse<*>, delayMillis: Int = 0): MappingBuilder =
139172
WireMock.post("/graphql")
140173
.willReturn(

0 commit comments

Comments
 (0)