From 55fdcba29536134c660227e2596204d085800c81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Quenaudon?= Date: Thu, 16 Apr 2026 10:29:02 +0100 Subject: [PATCH] Avoid race in duplex pipe for streaming calls --- wire-grpc-client/api/wire-grpc-client.api | 5 + .../kotlin/com/squareup/wire/GrpcMethod.kt | 6 +- .../kotlin/com/squareup/wire/GrpcClient.kt | 13 +- .../wire/internal/BlockingMessageSource.kt | 4 +- .../internal/RealGrpcServerStreamingCall.kt | 239 +++++++++++++++++- .../wire/internal/RealGrpcStreamingCall.kt | 13 +- .../kotlin/com/squareup/wire/internal/grpc.kt | 11 +- .../squareup/wire/GrpcOnMockWebServerTest.kt | 71 ++++++ .../squareup/wire/kotlin/KotlinGenerator.kt | 13 +- .../wire/kotlin/KotlinGeneratorTest.kt | 22 +- 10 files changed, 379 insertions(+), 18 deletions(-) diff --git a/wire-grpc-client/api/wire-grpc-client.api b/wire-grpc-client/api/wire-grpc-client.api index 644d1ed0ce..f67d01901e 100644 --- a/wire-grpc-client/api/wire-grpc-client.api +++ b/wire-grpc-client/api/wire-grpc-client.api @@ -81,9 +81,14 @@ public final class com/squareup/wire/GrpcHttpUrlKt { public final class com/squareup/wire/GrpcMethod { public fun (Ljava/lang/String;Lcom/squareup/wire/ProtoAdapter;Lcom/squareup/wire/ProtoAdapter;)V + public fun (Ljava/lang/String;Lcom/squareup/wire/ProtoAdapter;Lcom/squareup/wire/ProtoAdapter;Z)V + public fun (Ljava/lang/String;Lcom/squareup/wire/ProtoAdapter;Lcom/squareup/wire/ProtoAdapter;ZZ)V + public synthetic fun (Ljava/lang/String;Lcom/squareup/wire/ProtoAdapter;Lcom/squareup/wire/ProtoAdapter;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getPath ()Ljava/lang/String; public final fun getRequestAdapter ()Lcom/squareup/wire/ProtoAdapter; + public final fun getRequestStreaming ()Z public final fun getResponseAdapter ()Lcom/squareup/wire/ProtoAdapter; + public final fun getResponseStreaming ()Z } public abstract interface class com/squareup/wire/GrpcServerStreamingCall { diff --git a/wire-grpc-client/src/commonMain/kotlin/com/squareup/wire/GrpcMethod.kt b/wire-grpc-client/src/commonMain/kotlin/com/squareup/wire/GrpcMethod.kt index 3543797967..6a91190b67 100644 --- a/wire-grpc-client/src/commonMain/kotlin/com/squareup/wire/GrpcMethod.kt +++ b/wire-grpc-client/src/commonMain/kotlin/com/squareup/wire/GrpcMethod.kt @@ -15,8 +15,12 @@ */ package com.squareup.wire -class GrpcMethod( +import kotlin.jvm.JvmOverloads + +class GrpcMethod @JvmOverloads constructor( val path: String, val requestAdapter: ProtoAdapter, val responseAdapter: ProtoAdapter, + val requestStreaming: Boolean = false, + val responseStreaming: Boolean = false, ) diff --git a/wire-grpc-client/src/jvmMain/kotlin/com/squareup/wire/GrpcClient.kt b/wire-grpc-client/src/jvmMain/kotlin/com/squareup/wire/GrpcClient.kt index c43b1f6123..6d6b3b3523 100644 --- a/wire-grpc-client/src/jvmMain/kotlin/com/squareup/wire/GrpcClient.kt +++ b/wire-grpc-client/src/jvmMain/kotlin/com/squareup/wire/GrpcClient.kt @@ -16,9 +16,10 @@ package com.squareup.wire import com.squareup.wire.internal.RealGrpcCall +import com.squareup.wire.internal.RealGrpcServerStreamingCall import com.squareup.wire.internal.RealGrpcStreamingCall import com.squareup.wire.internal.asGrpcClientStreamingCall -import com.squareup.wire.internal.asGrpcServerStreamingCall +import com.squareup.wire.internal.asGrpcStreamingCall import java.util.concurrent.TimeUnit import kotlin.reflect.KClass import okhttp3.Call @@ -183,9 +184,15 @@ internal class WireGrpcClient internal constructor( ) : GrpcClient() { override fun newCall(method: GrpcMethod): GrpcCall = RealGrpcCall(this, method) - override fun newStreamingCall(method: GrpcMethod): GrpcStreamingCall = RealGrpcStreamingCall(this, method) + override fun newStreamingCall(method: GrpcMethod): GrpcStreamingCall { + return if (!method.requestStreaming && method.responseStreaming) { + RealGrpcServerStreamingCall(this, method).asGrpcStreamingCall() + } else { + RealGrpcStreamingCall(this, method) + } + } override fun newClientStreamingCall(method: GrpcMethod): GrpcClientStreamingCall = RealGrpcStreamingCall(this, method).asGrpcClientStreamingCall() - override fun newServerStreamingCall(method: GrpcMethod): GrpcServerStreamingCall = RealGrpcStreamingCall(this, method).asGrpcServerStreamingCall() + override fun newServerStreamingCall(method: GrpcMethod): GrpcServerStreamingCall = RealGrpcServerStreamingCall(this, method) } diff --git a/wire-grpc-client/src/jvmMain/kotlin/com/squareup/wire/internal/BlockingMessageSource.kt b/wire-grpc-client/src/jvmMain/kotlin/com/squareup/wire/internal/BlockingMessageSource.kt index 653ddaff38..7515c3a77f 100644 --- a/wire-grpc-client/src/jvmMain/kotlin/com/squareup/wire/internal/BlockingMessageSource.kt +++ b/wire-grpc-client/src/jvmMain/kotlin/com/squareup/wire/internal/BlockingMessageSource.kt @@ -33,7 +33,7 @@ import okio.IOException * * Complete: enqueued when the stream completes normally. */ internal class BlockingMessageSource( - val grpcCall: RealGrpcStreamingCall<*, R>, + val onResponseMetadata: (Map) -> Unit, val responseAdapter: ProtoAdapter, val call: Call, ) : MessageSource { @@ -66,7 +66,7 @@ internal class BlockingMessageSource( override fun onResponse(call: Call, response: Response) { try { - grpcCall.responseMetadata = response.headers.toMap() + onResponseMetadata(response.headers.toMap()) response.use { response.messageSource(responseAdapter).use { reader -> while (true) { diff --git a/wire-grpc-client/src/jvmMain/kotlin/com/squareup/wire/internal/RealGrpcServerStreamingCall.kt b/wire-grpc-client/src/jvmMain/kotlin/com/squareup/wire/internal/RealGrpcServerStreamingCall.kt index 50338d2b5c..d4b04b116b 100644 --- a/wire-grpc-client/src/jvmMain/kotlin/com/squareup/wire/internal/RealGrpcServerStreamingCall.kt +++ b/wire-grpc-client/src/jvmMain/kotlin/com/squareup/wire/internal/RealGrpcServerStreamingCall.kt @@ -18,12 +18,122 @@ package com.squareup.wire.internal import com.squareup.wire.GrpcMethod import com.squareup.wire.GrpcServerStreamingCall import com.squareup.wire.GrpcStreamingCall +import com.squareup.wire.MessageSink import com.squareup.wire.MessageSource +import com.squareup.wire.WireGrpcClient +import java.util.concurrent.TimeUnit import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import okio.ForwardingTimeout +import okio.IOException import okio.Timeout +/** + * A [GrpcServerStreamingCall] that sends a single non-duplex request and reads a streaming + * response. Using a non-duplex request body ensures the complete request (including END_STREAM) is + * sent to the server before responses are read, avoiding delays on servers that wait for the + * client's half-close before starting to stream responses. + */ internal class RealGrpcServerStreamingCall( + private val grpcClient: WireGrpcClient, + override val method: GrpcMethod, +) : GrpcServerStreamingCall { + + private var call: okhttp3.Call? = null + private var canceled = false + + override val timeout: Timeout = ForwardingTimeout(Timeout()) + + init { + timeout.clearTimeout() + timeout.clearDeadline() + } + + override var requestMetadata: Map = mapOf() + + override var responseMetadata: Map? = null + internal set + + override fun cancel() { + canceled = true + call?.cancel() + } + + override fun isCanceled(): Boolean = canceled || call?.isCanceled() == true + + override fun isExecuted(): Boolean = call?.isExecuted() ?: false + + override fun clone(): GrpcServerStreamingCall { + val result = RealGrpcServerStreamingCall(grpcClient, method) + val oldTimeout = this.timeout + result.timeout.also { newTimeout -> + newTimeout.timeout(oldTimeout.timeoutNanos(), TimeUnit.NANOSECONDS) + if (oldTimeout.hasDeadline()) { + newTimeout.deadlineNanoTime(oldTimeout.deadlineNanoTime()) + } else { + newTimeout.clearDeadline() + } + } + result.requestMetadata += this.requestMetadata + return result + } + + override suspend fun executeIn(scope: CoroutineScope, request: S): ReceiveChannel { + val responseChannel = Channel(1) + val call = initCall(request) + + responseChannel.invokeOnClose { cause -> + if (cause != null) { + call.cancel() + } + } + + call.enqueue( + responseChannel.readFromResponseBodyCallback( + onResponseMetadata = { this.responseMetadata = it }, + responseAdapter = method.responseAdapter, + ), + ) + + return responseChannel + } + + override fun executeBlocking(request: S): MessageSource { + val call = initCall(request) + val messageSource = BlockingMessageSource( + onResponseMetadata = { this.responseMetadata = it }, + responseAdapter = method.responseAdapter, + call = call, + ) + call.enqueue(messageSource.readFromResponseBodyCallback()) + return messageSource + } + + private fun initCall(request: S): okhttp3.Call { + check(this.call == null) { "already executed" } + val requestBody = newRequestBody( + minMessageToCompress = grpcClient.minMessageToCompress, + requestAdapter = method.requestAdapter, + onlyMessage = request, + ) + val result = grpcClient.newCall(method, requestMetadata, requestBody, timeout) + this.call = result + if (canceled) result.cancel() + (timeout as ForwardingTimeout).setDelegate(result.timeout()) + return result + } +} + +/** + * Wraps a [GrpcStreamingCall] as a [GrpcServerStreamingCall]. Used for test doubles created via + * [com.squareup.wire.GrpcServerStreamingCall] factory functions in GrpcCalls. + */ +internal class GrpcStreamingCallServerStreamingAdapter( private val callDelegate: GrpcStreamingCall, override val method: GrpcMethod, ) : GrpcServerStreamingCall { @@ -48,7 +158,7 @@ internal class RealGrpcServerStreamingCall( override fun isExecuted() = callDelegate.isExecuted() - override fun clone() = RealGrpcServerStreamingCall(callDelegate.clone(), method) + override fun clone() = GrpcStreamingCallServerStreamingAdapter(callDelegate.clone(), method) override suspend fun executeIn(scope: CoroutineScope, request: S): ReceiveChannel { val (sendChannel, receiveChannel) = callDelegate.executeIn(scope) @@ -67,4 +177,129 @@ internal class RealGrpcServerStreamingCall( } } -internal fun GrpcStreamingCall.asGrpcServerStreamingCall() = RealGrpcServerStreamingCall(this, method) +internal fun GrpcStreamingCall.asGrpcServerStreamingCall() = GrpcStreamingCallServerStreamingAdapter(this, method) + +/** + * Wraps a [GrpcServerStreamingCall] as the legacy [GrpcStreamingCall] API. This is used by + * generated clients when explicit streaming call types are disabled. + */ +internal class GrpcServerStreamingCallStreamingAdapter( + private val callDelegate: GrpcServerStreamingCall, + override val method: GrpcMethod, +) : GrpcStreamingCall { + private var executed = false + + override val timeout: Timeout + get() = callDelegate.timeout + + override var requestMetadata: Map + get() = callDelegate.requestMetadata + set(value) { + callDelegate.requestMetadata = value + } + + override val responseMetadata: Map? + get() = callDelegate.responseMetadata + + override fun cancel() { + callDelegate.cancel() + } + + override fun isCanceled() = callDelegate.isCanceled() + + @Suppress("OPT_IN_USAGE", "OVERRIDE_DEPRECATION") + override fun execute(): Pair, ReceiveChannel> { + return executeIn(GlobalScope) + } + + override fun executeIn(scope: CoroutineScope): Pair, ReceiveChannel> { + return executeWithChannels(scope) + } + + @Suppress("OPT_IN_USAGE") + override fun executeBlocking(): Pair, MessageSource> { + val (requestChannel, responseChannel) = executeWithChannels(GlobalScope) + return requestChannel.toMessageSink() to responseChannel.toMessageSource() + } + + override fun isExecuted() = executed || callDelegate.isExecuted() + + override fun clone() = GrpcServerStreamingCallStreamingAdapter(callDelegate.clone(), method) + + private fun executeWithChannels(scope: CoroutineScope): Pair, Channel> { + check(!executed) { "already executed" } + executed = true + + val requestChannel = Channel(1) + val responseChannel = Channel(1) + var delegateResponseChannel: ReceiveChannel? = null + + responseChannel.invokeOnClose { cause -> + if (cause != null) { + requestChannel.cancel() + delegateResponseChannel?.cancel() + callDelegate.cancel() + } + } + + scope.launch { + try { + val requestResult = requestChannel.receiveCatching() + requestResult.exceptionOrNull()?.let { throw it } + val request = requestResult.getOrNull() + ?: throw ProtocolException("expected 1 message but got none") + requestChannel.close() + val responses = callDelegate.executeIn(scope, request) + delegateResponseChannel = responses + for (response in responses) { + responseChannel.send(response) + } + responseChannel.close() + } catch (e: Throwable) { + responseChannel.close(e) + } + } + + return requestChannel to responseChannel + } +} + +internal fun GrpcServerStreamingCall.asGrpcStreamingCall() = GrpcServerStreamingCallStreamingAdapter(this, method) + +private fun Channel.toMessageSource() = object : MessageSource { + override fun read(): E? = runBlocking { + try { + val result = receiveCatching() + result.exceptionOrNull()?.let { throw it } + result.getOrNull() + } catch (e: Throwable) { + throw e.toIOException() + } + } + + override fun close() { + cancel() + } +} + +private fun Channel.toMessageSink() = object : MessageSink { + override fun write(message: E) { + runBlocking { + try { + send(message) + } catch (e: Throwable) { + throw e.toIOException() + } + } + } + + override fun cancel() { + this@toMessageSink.cancel() + } + + override fun close() { + this@toMessageSink.close() + } +} + +private fun Throwable.toIOException() = this as? IOException ?: IOException(this) diff --git a/wire-grpc-client/src/jvmMain/kotlin/com/squareup/wire/internal/RealGrpcStreamingCall.kt b/wire-grpc-client/src/jvmMain/kotlin/com/squareup/wire/internal/RealGrpcStreamingCall.kt index ea7ca3b504..108a00175f 100644 --- a/wire-grpc-client/src/jvmMain/kotlin/com/squareup/wire/internal/RealGrpcStreamingCall.kt +++ b/wire-grpc-client/src/jvmMain/kotlin/com/squareup/wire/internal/RealGrpcStreamingCall.kt @@ -90,14 +90,23 @@ internal class RealGrpcStreamingCall( callForCancel = call, ) } - call.enqueue(responseChannel.readFromResponseBodyCallback(this, method.responseAdapter)) + call.enqueue( + responseChannel.readFromResponseBodyCallback( + onResponseMetadata = { this.responseMetadata = it }, + responseAdapter = method.responseAdapter, + ), + ) return requestChannel to responseChannel } override fun executeBlocking(): Pair, MessageSource> { val call = initCall() - val messageSource = BlockingMessageSource(this, method.responseAdapter, call) + val messageSource = BlockingMessageSource( + onResponseMetadata = { this.responseMetadata = it }, + responseAdapter = method.responseAdapter, + call = call, + ) val messageSink = requestBody.messageSink( minMessageToCompress = grpcClient.minMessageToCompress, requestAdapter = method.requestAdapter, diff --git a/wire-grpc-client/src/jvmMain/kotlin/com/squareup/wire/internal/grpc.kt b/wire-grpc-client/src/jvmMain/kotlin/com/squareup/wire/internal/grpc.kt index 7c894a93ab..bcef8aba27 100644 --- a/wire-grpc-client/src/jvmMain/kotlin/com/squareup/wire/internal/grpc.kt +++ b/wire-grpc-client/src/jvmMain/kotlin/com/squareup/wire/internal/grpc.kt @@ -83,6 +83,15 @@ internal fun PipeDuplexRequestBody.messageSink( internal fun SendChannel.readFromResponseBodyCallback( grpcCall: RealGrpcStreamingCall<*, R>, responseAdapter: ProtoAdapter, +): Callback = readFromResponseBodyCallback( + onResponseMetadata = { grpcCall.responseMetadata = it }, + responseAdapter = responseAdapter, +) + +/** Sends the response messages to the channel. */ +internal fun SendChannel.readFromResponseBodyCallback( + onResponseMetadata: (Map) -> Unit, + responseAdapter: ProtoAdapter, ): Callback { return object : Callback { override fun onFailure(call: Call, e: IOException) { @@ -91,7 +100,7 @@ internal fun SendChannel.readFromResponseBodyCallback( } override fun onResponse(call: Call, response: Response) { - grpcCall.responseMetadata = response.headers.toMap() + onResponseMetadata(response.headers.toMap()) runBlocking { response.use { val messageSource = try { diff --git a/wire-grpc-tests/src/test/java/com/squareup/wire/GrpcOnMockWebServerTest.kt b/wire-grpc-tests/src/test/java/com/squareup/wire/GrpcOnMockWebServerTest.kt index 3578ff85c0..5e53f34785 100644 --- a/wire-grpc-tests/src/test/java/com/squareup/wire/GrpcOnMockWebServerTest.kt +++ b/wire-grpc-tests/src/test/java/com/squareup/wire/GrpcOnMockWebServerTest.kt @@ -18,6 +18,8 @@ package com.squareup.wire import assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isNull import com.squareup.wire.mockwebserver.GrpcDispatcher import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicReference @@ -25,10 +27,13 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ObsoleteCoroutinesApi import kotlinx.coroutines.runBlocking import okhttp3.Call +import okhttp3.Headers.Companion.headersOf import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Protocol +import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer +import okio.Buffer import org.junit.Before import org.junit.Rule import org.junit.Test @@ -82,6 +87,72 @@ class GrpcOnMockWebServerTest { routeGuideService = grpcClient.create(RouteGuideClient::class) } + @Test + fun serverStreamingListFeatures() { + // MockWebServer only dispatches after receiving the complete request body including END_STREAM, + // mimicking some server behaviors that would cause hanging until timeout when + // GrpcServerStreamingCall used a duplex request body. + enqueueListFeaturesResponse() + + runBlocking { + val listFeatures = routeGuideService.ListFeatures() + val responses = listFeatures.executeIn( + this, + Rectangle(lo = Point(latitude = 1, longitude = 2), hi = Point(latitude = 3, longitude = 4)), + ) + assertThat(responses.receive()).isEqualTo(Feature(name = "peak")) + assertThat(responses.receive()).isEqualTo(Feature(name = "valley")) + assertThat(responses.receiveCatching().getOrNull()).isNull() + assertThat(listFeatures.isCanceled()).isFalse() + } + } + + @Test + fun legacyServerStreamingListFeatures() { + enqueueListFeaturesResponse() + + val listFeatures = grpcClient.newStreamingCall( + GrpcMethod( + path = "/routeguide.RouteGuide/ListFeatures", + requestAdapter = Rectangle.ADAPTER, + responseAdapter = Feature.ADAPTER, + responseStreaming = true, + ), + ) + + runBlocking { + val (requests, responses) = listFeatures.executeIn(this) + requests.send(Rectangle(lo = Point(latitude = 1, longitude = 2), hi = Point(latitude = 3, longitude = 4))) + requests.close() + assertThat(responses.receive()).isEqualTo(Feature(name = "peak")) + assertThat(responses.receive()).isEqualTo(Feature(name = "valley")) + assertThat(responses.receiveCatching().getOrNull()).isNull() + assertThat(listFeatures.isCanceled()).isFalse() + } + } + + private fun enqueueListFeaturesResponse() { + val responseBody = Buffer() + for (feature in listOf(Feature(name = "peak"), Feature(name = "valley"))) { + val encoded = Feature.ADAPTER.encodeByteString(feature) + responseBody.writeByte(0) // not compressed + responseBody.writeInt(encoded.size) + responseBody.write(encoded) + } + val grpcDispatcher = mockWebServer.dispatcher + mockWebServer.dispatcher = object : okhttp3.mockwebserver.Dispatcher() { + override fun dispatch(request: okhttp3.mockwebserver.RecordedRequest): MockResponse { + if (request.path == "/routeguide.RouteGuide/ListFeatures") { + return MockResponse() + .setHeader("Content-Type", "application/grpc") + .setTrailers(headersOf("grpc-status", "0")) + .setBody(responseBody) + } + return grpcDispatcher.dispatch(request) + } + } + } + @Test fun requestResponseSuspend() { runBlocking { diff --git a/wire-kotlin-generator/src/main/java/com/squareup/wire/kotlin/KotlinGenerator.kt b/wire-kotlin-generator/src/main/java/com/squareup/wire/kotlin/KotlinGenerator.kt index 413d0b67ac..586fd69e4b 100644 --- a/wire-kotlin-generator/src/main/java/com/squareup/wire/kotlin/KotlinGenerator.kt +++ b/wire-kotlin-generator/src/main/java/com/squareup/wire/kotlin/KotlinGenerator.kt @@ -430,7 +430,18 @@ class KotlinGenerator private constructor( .addStatement("%T(⇥⇥", GrpcMethod::class) .addStatement("path = %S,", "/$packageName$serviceName/${rpc.name}") .addStatement("requestAdapter = %L,", rpc.requestType!!.getAdapterName()) - .addStatement("responseAdapter = %L", rpc.responseType!!.getAdapterName()) + .apply { + val streamingArguments = buildList { + if (rpc.requestStreaming) add("requestStreaming = true") + if (rpc.responseStreaming) add("responseStreaming = true") + } + val responseAdapterSuffix = if (streamingArguments.isEmpty()) "" else "," + addStatement("responseAdapter = %L$responseAdapterSuffix", rpc.responseType!!.getAdapterName()) + for ((index, argument) in streamingArguments.withIndex()) { + val suffix = if (index == streamingArguments.lastIndex) "" else "," + addStatement("$argument$suffix") + } + } .add("⇤⇤)") .build() when { diff --git a/wire-kotlin-generator/src/test/java/com/squareup/wire/kotlin/KotlinGeneratorTest.kt b/wire-kotlin-generator/src/test/java/com/squareup/wire/kotlin/KotlinGeneratorTest.kt index 34f4a9452d..a35d4a90d7 100644 --- a/wire-kotlin-generator/src/test/java/com/squareup/wire/kotlin/KotlinGeneratorTest.kt +++ b/wire-kotlin-generator/src/test/java/com/squareup/wire/kotlin/KotlinGeneratorTest.kt @@ -724,7 +724,8 @@ class KotlinGeneratorTest { | override fun RecordRoute(): GrpcStreamingCall = client.newStreamingCall(GrpcMethod( | path = "/routeguide.RouteGuide/RecordRoute", | requestAdapter = Point.ADAPTER, - | responseAdapter = RouteSummary.ADAPTER + | responseAdapter = RouteSummary.ADAPTER, + | requestStreaming = true | )) |} | @@ -791,7 +792,8 @@ class KotlinGeneratorTest { | override fun ListFeatures(): GrpcStreamingCall = client.newStreamingCall(GrpcMethod( | path = "/routeguide.RouteGuide/ListFeatures", | requestAdapter = Rectangle.ADAPTER, - | responseAdapter = Feature.ADAPTER + | responseAdapter = Feature.ADAPTER, + | responseStreaming = true | )) |} | @@ -861,7 +863,9 @@ class KotlinGeneratorTest { override fun RouteChat(): GrpcStreamingCall = client.newStreamingCall(GrpcMethod( path = "/routeguide.RouteGuide/RouteChat", requestAdapter = RouteNote.ADAPTER, - responseAdapter = RouteNote.ADAPTER + responseAdapter = RouteNote.ADAPTER, + requestStreaming = true, + responseStreaming = true )) } @@ -930,7 +934,9 @@ class KotlinGeneratorTest { override fun RouteChat(): GrpcStreamingCall = client.newStreamingCall(GrpcMethod( path = "/routeguide.RouteGuide/RouteChat", requestAdapter = RouteNote.ADAPTER, - responseAdapter = RouteNote.ADAPTER + responseAdapter = RouteNote.ADAPTER, + requestStreaming = true, + responseStreaming = true )) } @@ -1065,7 +1071,9 @@ class KotlinGeneratorTest { | override fun RouteChat(): GrpcStreamingCall = client.newStreamingCall(GrpcMethod( | path = "/routeguide.RouteGuide/RouteChat", | requestAdapter = RouteNote.ADAPTER, - | responseAdapter = RouteNote.ADAPTER + | responseAdapter = RouteNote.ADAPTER, + | requestStreaming = true, + | responseStreaming = true | )) |} | @@ -1178,7 +1186,9 @@ class KotlinGeneratorTest { | override fun RouteChat(): GrpcStreamingCall = client.newStreamingCall(GrpcMethod( | path = "/routeguide.RouteGuide/RouteChat", | requestAdapter = RouteNote.ADAPTER, - | responseAdapter = RouteNote.ADAPTER + | responseAdapter = RouteNote.ADAPTER, + | requestStreaming = true, + | responseStreaming = true | )) |} |