Skip to content

Commit 1daf6d2

Browse files
committed
Parse trailers even when an error occurs
It is possible for gRPC error responses to be empty and for servers to send the error information within trailers without also sending them in headers. In these cases, Wire will propagate an exception due to no response body and will not attempt to read trailers, relying only on headers instead. This patch allows Wire to attempt to parse the trailers for non-streaming requests instead of only relying on headers.
1 parent 5246ce0 commit 1daf6d2

File tree

3 files changed

+44
-2
lines changed

3 files changed

+44
-2
lines changed

wire-grpc-client/src/commonMain/kotlin/com/squareup/wire/internal/GrpcMessageSource.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ internal class GrpcMessageSource<T : Any>(
6161
}
6262
}
6363

64+
fun isEmptyBody(): Boolean = source.exhausted()
65+
6466
fun readExactlyOneAndClose(): T {
6567
use(GrpcMessageSource<T>::close) { reader ->
6668
val result = reader.read() ?: throw ProtocolException("expected 1 message but got none")

wire-grpc-client/src/jvmMain/kotlin/com/squareup/wire/internal/RealGrpcCall.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,19 @@ internal class RealGrpcCall<S : Any, R : Any>(
104104
use {
105105
messageSource(method.responseAdapter).use { reader ->
106106
val result = try {
107-
reader.readExactlyOneAndClose()
107+
if (reader.isEmptyBody()) {
108+
// an empty body is valid for error responses. return null so that we try to extract
109+
// the error from the trailers.
110+
null
111+
} else {
112+
reader.readExactlyOneAndClose()
113+
}
108114
} catch (e: IOException) {
109115
throw grpcResponseToException(e)!!
110116
}
111117
val exception = grpcResponseToException()
112118
if (exception != null) throw exception
113-
return result
119+
return result ?: throw grpcResponseToException(ProtocolException("expected 1 message but got none"))!!
114120
}
115121
}
116122
}

wire-grpc-tests/src/test/java/com/squareup/wire/GrpcClientTest.kt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ import kotlinx.coroutines.runBlocking
5555
import kotlinx.coroutines.sync.Mutex
5656
import kotlinx.coroutines.sync.withLock
5757
import okhttp3.Call
58+
import okhttp3.ExperimentalOkHttpApi
59+
import okhttp3.Headers
5860
import okhttp3.Interceptor
5961
import okhttp3.Interceptor.Chain
6062
import okhttp3.MediaType.Companion.toMediaType
@@ -1231,6 +1233,38 @@ class GrpcClientTest {
12311233
}
12321234
}
12331235

1236+
@Test
1237+
fun requestFailureInTrailersNotInHeadersWithEmptyResponseBody() {
1238+
mockService.enqueue(ReceiveCall("/routeguide.RouteGuide/RouteChat"))
1239+
mockService.enqueueReceivePoint(latitude = 5, longitude = 6)
1240+
mockService.enqueue(ReceiveComplete)
1241+
mockService.enqueueSendError(
1242+
Status.UNAUTHENTICATED.withDescription("not logged in")
1243+
.asRuntimeException(),
1244+
)
1245+
mockService.enqueue(SendCompleted)
1246+
1247+
@OptIn(ExperimentalOkHttpApi::class)
1248+
interceptor = object : Interceptor {
1249+
override fun intercept(chain: Chain): Response {
1250+
val response = chain.proceed(chain.request())
1251+
return response.newBuilder()
1252+
.headers(Headers.headersOf())
1253+
.trailers { response.headers }
1254+
.build()
1255+
}
1256+
}
1257+
1258+
val grpcCall = routeGuideService.GetFeature()
1259+
try {
1260+
grpcCall.executeBlocking(Point(latitude = 5, longitude = 6))
1261+
fail()
1262+
} catch (expected: GrpcException) {
1263+
assertThat(expected.grpcStatus).isEqualTo(GrpcStatus.UNAUTHENTICATED)
1264+
assertThat(expected.grpcMessage).isEqualTo("not logged in")
1265+
}
1266+
}
1267+
12341268
@Test
12351269
fun requestEarlyFailureWithDescription() {
12361270
mockService.enqueue(ReceiveCall("/routeguide.RouteGuide/GetFeature"))

0 commit comments

Comments
 (0)