diff --git a/wire-grpc-client/src/commonMain/kotlin/com/squareup/wire/internal/GrpcMessageSource.kt b/wire-grpc-client/src/commonMain/kotlin/com/squareup/wire/internal/GrpcMessageSource.kt index fa91dea397..20c41211ca 100644 --- a/wire-grpc-client/src/commonMain/kotlin/com/squareup/wire/internal/GrpcMessageSource.kt +++ b/wire-grpc-client/src/commonMain/kotlin/com/squareup/wire/internal/GrpcMessageSource.kt @@ -61,6 +61,8 @@ internal class GrpcMessageSource( } } + fun isEmptyBody(): Boolean = source.exhausted() + fun readExactlyOneAndClose(): T { use(GrpcMessageSource::close) { reader -> val result = reader.read() ?: throw ProtocolException("expected 1 message but got none") diff --git a/wire-grpc-client/src/jvmMain/kotlin/com/squareup/wire/internal/RealGrpcCall.kt b/wire-grpc-client/src/jvmMain/kotlin/com/squareup/wire/internal/RealGrpcCall.kt index cc7fa79b18..19a6e7a5d1 100644 --- a/wire-grpc-client/src/jvmMain/kotlin/com/squareup/wire/internal/RealGrpcCall.kt +++ b/wire-grpc-client/src/jvmMain/kotlin/com/squareup/wire/internal/RealGrpcCall.kt @@ -104,13 +104,18 @@ internal class RealGrpcCall( use { messageSource(method.responseAdapter).use { reader -> val result = try { - reader.readExactlyOneAndClose() + if (reader.isEmptyBody()) { + // an empty body is valid for error responses. don't throw so we try to parse trailers. + null + } else { + reader.readExactlyOneAndClose() + } } catch (e: IOException) { throw grpcResponseToException(e)!! } val exception = grpcResponseToException() if (exception != null) throw exception - return result + return result ?: throw grpcResponseToException(ProtocolException("expected 1 message but got none"))!! } } } diff --git a/wire-grpc-tests/src/test/java/com/squareup/wire/GrpcClientTest.kt b/wire-grpc-tests/src/test/java/com/squareup/wire/GrpcClientTest.kt index 52a2c083f5..6ed38b8768 100644 --- a/wire-grpc-tests/src/test/java/com/squareup/wire/GrpcClientTest.kt +++ b/wire-grpc-tests/src/test/java/com/squareup/wire/GrpcClientTest.kt @@ -55,6 +55,8 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import okhttp3.Call +import okhttp3.ExperimentalOkHttpApi +import okhttp3.Headers import okhttp3.Interceptor import okhttp3.Interceptor.Chain import okhttp3.MediaType.Companion.toMediaType @@ -1231,6 +1233,38 @@ class GrpcClientTest { } } + @Test + fun requestFailureInTrailersNotInHeadersWithEmptyResponseBody() { + mockService.enqueue(ReceiveCall("/routeguide.RouteGuide/RouteChat")) + mockService.enqueueReceivePoint(latitude = 5, longitude = 6) + mockService.enqueue(ReceiveComplete) + mockService.enqueueSendError( + Status.UNAUTHENTICATED.withDescription("not logged in") + .asRuntimeException(), + ) + mockService.enqueue(SendCompleted) + + @OptIn(ExperimentalOkHttpApi::class) + interceptor = object : Interceptor { + override fun intercept(chain: Chain): Response { + val response = chain.proceed(chain.request()) + return response.newBuilder() + .headers(Headers.headersOf()) + .trailers { response.headers } + .build() + } + } + + val grpcCall = routeGuideService.GetFeature() + try { + grpcCall.executeBlocking(Point(latitude = 5, longitude = 6)) + fail() + } catch (expected: GrpcException) { + assertThat(expected.grpcStatus).isEqualTo(GrpcStatus.UNAUTHENTICATED) + assertThat(expected.grpcMessage).isEqualTo("not logged in") + } + } + @Test fun requestEarlyFailureWithDescription() { mockService.enqueue(ReceiveCall("/routeguide.RouteGuide/GetFeature"))