From 3c4b25adc3cb7d94f9440435841ff8f6cb7f1657 Mon Sep 17 00:00:00 2001 From: Zhumazhenis Date: Thu, 31 Jul 2025 01:07:16 +0100 Subject: [PATCH 01/10] Add interceptor for propagating gRPC Metadata to coroutineContext. --- stub/src/main/java/io/grpc/kotlin/Helpers.kt | 13 +++ .../MetadataCoroutineContextInterceptor.kt | 33 ++++++++ ...MetadataCoroutineContextInterceptorTest.kt | 81 +++++++++++++++++++ 3 files changed, 127 insertions(+) create mode 100644 stub/src/main/java/io/grpc/kotlin/MetadataCoroutineContextInterceptor.kt create mode 100644 stub/src/test/java/io/grpc/kotlin/MetadataCoroutineContextInterceptorTest.kt diff --git a/stub/src/main/java/io/grpc/kotlin/Helpers.kt b/stub/src/main/java/io/grpc/kotlin/Helpers.kt index bc0e2728..50d443ec 100644 --- a/stub/src/main/java/io/grpc/kotlin/Helpers.kt +++ b/stub/src/main/java/io/grpc/kotlin/Helpers.kt @@ -16,8 +16,10 @@ package io.grpc.kotlin +import io.grpc.Metadata import io.grpc.Status import io.grpc.StatusException +import kotlin.coroutines.coroutineContext import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -75,3 +77,14 @@ internal fun Flow.singleOrStatusFlow(expected: String, descriptor: Any): */ internal suspend fun Flow.singleOrStatus(expected: String, descriptor: Any): T = singleOrStatusFlow(expected, descriptor).single() + +/** + * Returns gRPC Metadata. + */ +suspend fun grpcMetadata(): Metadata { + val metadataElement = coroutineContext[MetadataElement] + ?: throw Status.INTERNAL + .withDescription("gRPC Metadata not found in coroutineContext. Ensure that MetadataCoroutineContextInterceptor is used in gRPC server.") + .asException() + return metadataElement.value +} diff --git a/stub/src/main/java/io/grpc/kotlin/MetadataCoroutineContextInterceptor.kt b/stub/src/main/java/io/grpc/kotlin/MetadataCoroutineContextInterceptor.kt new file mode 100644 index 00000000..6dff8010 --- /dev/null +++ b/stub/src/main/java/io/grpc/kotlin/MetadataCoroutineContextInterceptor.kt @@ -0,0 +1,33 @@ +package io.grpc.kotlin + +import io.grpc.Metadata +import io.grpc.ServerCall +import kotlin.coroutines.CoroutineContext + +/** + * Propagates gRPC Metadata (HTTP Headers) to coroutineContext. + * Attach the interceptor to gRPC Server and then access the Metadata using extractMetadata() function. + * Example usage: + * + * ServerBuilder.forPort(8060) + * .addService(GreeterImpl()) + * .intercept(MetadataCoroutineContextInterceptor()) + * + * extractMetadata() + */ +class MetadataCoroutineContextInterceptor : CoroutineContextServerInterceptor() { + final override fun coroutineContext(call: ServerCall<*, *>, headers: Metadata): CoroutineContext { + return MetadataElement(value = headers) + } +} + +/** + * Used for accessing the Metadata from coroutineContext. + * Example usage: + * coroutineContext[MetadataElement]?.value + */ +internal data class MetadataElement(val value: Metadata) : CoroutineContext.Element { + companion object Key : CoroutineContext.Key + + override val key: CoroutineContext.Key get() = Key +} diff --git a/stub/src/test/java/io/grpc/kotlin/MetadataCoroutineContextInterceptorTest.kt b/stub/src/test/java/io/grpc/kotlin/MetadataCoroutineContextInterceptorTest.kt new file mode 100644 index 00000000..cf59886e --- /dev/null +++ b/stub/src/test/java/io/grpc/kotlin/MetadataCoroutineContextInterceptorTest.kt @@ -0,0 +1,81 @@ +package io.grpc.kotlin + +import io.grpc.BindableService +import io.grpc.Channel +import io.grpc.Metadata +import io.grpc.Status +import io.grpc.StatusException +import io.grpc.examples.helloworld.GreeterGrpcKt +import io.grpc.examples.helloworld.HelloReply +import io.grpc.examples.helloworld.HelloRequest +import io.grpc.inprocess.InProcessChannelBuilder +import io.grpc.inprocess.InProcessServerBuilder +import io.grpc.testing.GrpcCleanupRule +import kotlinx.coroutines.runBlocking +import org.junit.Rule +import org.junit.Test +import org.junit.jupiter.api.Assertions +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class MetadataCoroutineContextInterceptorTest { + @Rule + @JvmField + val grpcCleanup = GrpcCleanupRule() + + @Test + fun `interceptor provides gRPC Metadata to coroutineContext`() { + val key = Metadata.Key.of("test-header", Metadata.ASCII_STRING_MARSHALLER) + val clientStub = + GreeterGrpcKt.GreeterCoroutineStub(testChannel(object : GreeterGrpcKt.GreeterCoroutineImplBase() { + override suspend fun sayHello(request: HelloRequest): HelloReply { + val metadata = grpcMetadata() + return HelloReply.newBuilder() + .setMessage(metadata.get(key).toString()) + .build() + } + })) + val metadata = Metadata() + metadata.put(key, "Test message") + + val response = runBlocking { clientStub.sayHello(HelloRequest.getDefaultInstance(), metadata) } + + Assertions.assertEquals("Test message", response.message) + } + + @Test + fun `fails to extract gRPC Metadata if interceptor is not injected`() { + val key = Metadata.Key.of("test-header", Metadata.ASCII_STRING_MARSHALLER) + val clientStub = + GreeterGrpcKt.GreeterCoroutineStub(testChannel(object : GreeterGrpcKt.GreeterCoroutineImplBase() { + override suspend fun sayHello(request: HelloRequest): HelloReply { + val metadata = grpcMetadata() + return HelloReply.newBuilder() + .setMessage(metadata.get(key).toString()) + .build() + } + }, false)) + val metadata = Metadata() + metadata.put(key, "Test message") + + val exception = Assertions.assertThrows(StatusException::class.java) { + runBlocking { clientStub.sayHello(HelloRequest.getDefaultInstance(), metadata) } + } + Assertions.assertEquals(Status.INTERNAL.code, exception.status.code) + Assertions.assertEquals( + "gRPC Metadata not found in coroutineContext. Ensure that MetadataCoroutineContextInterceptor is used in gRPC server.", + exception.status.description + ) + } + + private fun testChannel(service: BindableService, attachInterceptor: Boolean = true): Channel { + val serverName = InProcessServerBuilder.generateName() + var builder = InProcessServerBuilder.forName(serverName).directExecutor() + if (attachInterceptor) { + builder = builder.intercept(MetadataCoroutineContextInterceptor()) + } + grpcCleanup.register(builder.addService(service).build().start()) + return grpcCleanup.register(InProcessChannelBuilder.forName(serverName).directExecutor().build()) + } +} From 02ea177bf1ce8c85a88c936a0416661769312ca6 Mon Sep 17 00:00:00 2001 From: Zhumazhenis Date: Thu, 31 Jul 2025 01:58:12 +0100 Subject: [PATCH 02/10] Refactor. --- .../io/grpc/kotlin/MetadataCoroutineContextInterceptor.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/stub/src/main/java/io/grpc/kotlin/MetadataCoroutineContextInterceptor.kt b/stub/src/main/java/io/grpc/kotlin/MetadataCoroutineContextInterceptor.kt index 6dff8010..4cbb9214 100644 --- a/stub/src/main/java/io/grpc/kotlin/MetadataCoroutineContextInterceptor.kt +++ b/stub/src/main/java/io/grpc/kotlin/MetadataCoroutineContextInterceptor.kt @@ -6,14 +6,15 @@ import kotlin.coroutines.CoroutineContext /** * Propagates gRPC Metadata (HTTP Headers) to coroutineContext. - * Attach the interceptor to gRPC Server and then access the Metadata using extractMetadata() function. + * Attach the interceptor to gRPC Server and then access the Metadata using grpcMetadata() function. + * * Example usage: * * ServerBuilder.forPort(8060) * .addService(GreeterImpl()) * .intercept(MetadataCoroutineContextInterceptor()) * - * extractMetadata() + * grpcMetadata() */ class MetadataCoroutineContextInterceptor : CoroutineContextServerInterceptor() { final override fun coroutineContext(call: ServerCall<*, *>, headers: Metadata): CoroutineContext { @@ -22,7 +23,7 @@ class MetadataCoroutineContextInterceptor : CoroutineContextServerInterceptor() } /** - * Used for accessing the Metadata from coroutineContext. + * Used for accessing the gRPC Metadata from coroutineContext. * Example usage: * coroutineContext[MetadataElement]?.value */ From 624ece828dcb194976b17f668b1bc694177927e9 Mon Sep 17 00:00:00 2001 From: Zhumazhenis Date: Thu, 18 Sep 2025 23:33:02 +0100 Subject: [PATCH 03/10] Refactor. --- stub/build.gradle.kts | 1 + stub/src/main/java/io/grpc/kotlin/Helpers.kt | 2 +- .../MetadataCoroutineContextInterceptor.kt | 7 ++++--- .../MetadataCoroutineContextInterceptorTest.kt | 18 +++++++++--------- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/stub/build.gradle.kts b/stub/build.gradle.kts index 6e00b59a..99a58681 100644 --- a/stub/build.gradle.kts +++ b/stub/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { api(libs.javax.annotation.api) // Testing + testImplementation(kotlin("test")) testImplementation(libs.junit) testImplementation(libs.junit.jupiter.engine) testImplementation(libs.truth.proto.extension) diff --git a/stub/src/main/java/io/grpc/kotlin/Helpers.kt b/stub/src/main/java/io/grpc/kotlin/Helpers.kt index 50d443ec..a6b9c098 100644 --- a/stub/src/main/java/io/grpc/kotlin/Helpers.kt +++ b/stub/src/main/java/io/grpc/kotlin/Helpers.kt @@ -85,6 +85,6 @@ suspend fun grpcMetadata(): Metadata { val metadataElement = coroutineContext[MetadataElement] ?: throw Status.INTERNAL .withDescription("gRPC Metadata not found in coroutineContext. Ensure that MetadataCoroutineContextInterceptor is used in gRPC server.") - .asException() + .asRuntimeException() return metadataElement.value } diff --git a/stub/src/main/java/io/grpc/kotlin/MetadataCoroutineContextInterceptor.kt b/stub/src/main/java/io/grpc/kotlin/MetadataCoroutineContextInterceptor.kt index 4cbb9214..768960d6 100644 --- a/stub/src/main/java/io/grpc/kotlin/MetadataCoroutineContextInterceptor.kt +++ b/stub/src/main/java/io/grpc/kotlin/MetadataCoroutineContextInterceptor.kt @@ -5,8 +5,8 @@ import io.grpc.ServerCall import kotlin.coroutines.CoroutineContext /** - * Propagates gRPC Metadata (HTTP Headers) to coroutineContext. - * Attach the interceptor to gRPC Server and then access the Metadata using grpcMetadata() function. + * A server interceptor which propagates gRPC Metadata (HTTP Headers) to coroutineContext. + * To use it attach the interceptor to gRPC Server and then access the Metadata using grpcMetadata() function. * * Example usage: * @@ -23,7 +23,8 @@ class MetadataCoroutineContextInterceptor : CoroutineContextServerInterceptor() } /** - * Used for accessing the gRPC Metadata from coroutineContext. + * A metadata element for coroutine context. + * It is used for accessing the gRPC Metadata from coroutineContext. * Example usage: * coroutineContext[MetadataElement]?.value */ diff --git a/stub/src/test/java/io/grpc/kotlin/MetadataCoroutineContextInterceptorTest.kt b/stub/src/test/java/io/grpc/kotlin/MetadataCoroutineContextInterceptorTest.kt index cf59886e..43ffea11 100644 --- a/stub/src/test/java/io/grpc/kotlin/MetadataCoroutineContextInterceptorTest.kt +++ b/stub/src/test/java/io/grpc/kotlin/MetadataCoroutineContextInterceptorTest.kt @@ -11,12 +11,13 @@ import io.grpc.examples.helloworld.HelloRequest import io.grpc.inprocess.InProcessChannelBuilder import io.grpc.inprocess.InProcessServerBuilder import io.grpc.testing.GrpcCleanupRule +import kotlin.test.assertFailsWith import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test -import org.junit.jupiter.api.Assertions import org.junit.runner.RunWith import org.junit.runners.JUnit4 +import com.google.common.truth.Truth.assertThat @RunWith(JUnit4::class) class MetadataCoroutineContextInterceptorTest { @@ -41,7 +42,7 @@ class MetadataCoroutineContextInterceptorTest { val response = runBlocking { clientStub.sayHello(HelloRequest.getDefaultInstance(), metadata) } - Assertions.assertEquals("Test message", response.message) + assertThat(response.message).isEqualTo("Test message") } @Test @@ -59,14 +60,13 @@ class MetadataCoroutineContextInterceptorTest { val metadata = Metadata() metadata.put(key, "Test message") - val exception = Assertions.assertThrows(StatusException::class.java) { - runBlocking { clientStub.sayHello(HelloRequest.getDefaultInstance(), metadata) } + val exception = assertFailsWith { + runBlocking { + clientStub.sayHello(HelloRequest.getDefaultInstance(), metadata) + } } - Assertions.assertEquals(Status.INTERNAL.code, exception.status.code) - Assertions.assertEquals( - "gRPC Metadata not found in coroutineContext. Ensure that MetadataCoroutineContextInterceptor is used in gRPC server.", - exception.status.description - ) + assertThat(exception.status.code).isEqualTo(Status.INTERNAL.code) + assertThat("gRPC Metadata not found in coroutineContext. Ensure that MetadataCoroutineContextInterceptor is used in gRPC server.").isEqualTo(exception.status.description) } private fun testChannel(service: BindableService, attachInterceptor: Boolean = true): Channel { From 504b3a7366055896c2b987f0f3d498c5954570ee Mon Sep 17 00:00:00 2001 From: Zhumazhenis Date: Mon, 22 Sep 2025 00:44:20 +0100 Subject: [PATCH 04/10] Refactor returning exception. --- stub/src/main/java/io/grpc/kotlin/Helpers.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/stub/src/main/java/io/grpc/kotlin/Helpers.kt b/stub/src/main/java/io/grpc/kotlin/Helpers.kt index a6b9c098..ededb717 100644 --- a/stub/src/main/java/io/grpc/kotlin/Helpers.kt +++ b/stub/src/main/java/io/grpc/kotlin/Helpers.kt @@ -83,8 +83,9 @@ internal suspend fun Flow.singleOrStatus(expected: String, descriptor: An */ suspend fun grpcMetadata(): Metadata { val metadataElement = coroutineContext[MetadataElement] - ?: throw Status.INTERNAL - .withDescription("gRPC Metadata not found in coroutineContext. Ensure that MetadataCoroutineContextInterceptor is used in gRPC server.") - .asRuntimeException() + ?: throw StatusException( + Status.INTERNAL + .withDescription("gRPC Metadata not found in coroutineContext. Ensure that MetadataCoroutineContextInterceptor is used in gRPC server.") + ) return metadataElement.value } From 6263fd860124eef1f3adf089121f1730d94a52f1 Mon Sep 17 00:00:00 2001 From: Zhumazhenis Date: Mon, 22 Sep 2025 01:46:44 +0100 Subject: [PATCH 05/10] Format. --- stub/src/main/java/io/grpc/kotlin/Helpers.kt | 16 +-- .../MetadataCoroutineContextInterceptor.kt | 26 ++--- ...MetadataCoroutineContextInterceptorTest.kt | 109 ++++++++++-------- 3 files changed, 78 insertions(+), 73 deletions(-) diff --git a/stub/src/main/java/io/grpc/kotlin/Helpers.kt b/stub/src/main/java/io/grpc/kotlin/Helpers.kt index ededb717..a9fe3468 100644 --- a/stub/src/main/java/io/grpc/kotlin/Helpers.kt +++ b/stub/src/main/java/io/grpc/kotlin/Helpers.kt @@ -78,14 +78,14 @@ internal fun Flow.singleOrStatusFlow(expected: String, descriptor: Any): internal suspend fun Flow.singleOrStatus(expected: String, descriptor: Any): T = singleOrStatusFlow(expected, descriptor).single() -/** - * Returns gRPC Metadata. - */ +/** Returns gRPC Metadata. */ suspend fun grpcMetadata(): Metadata { - val metadataElement = coroutineContext[MetadataElement] - ?: throw StatusException( - Status.INTERNAL - .withDescription("gRPC Metadata not found in coroutineContext. Ensure that MetadataCoroutineContextInterceptor is used in gRPC server.") - ) + val metadataElement = + coroutineContext[MetadataElement] + ?: throw StatusException( + Status.INTERNAL.withDescription( + "gRPC Metadata not found in coroutineContext. Ensure that MetadataCoroutineContextInterceptor is used in gRPC server." + ) + ) return metadataElement.value } diff --git a/stub/src/main/java/io/grpc/kotlin/MetadataCoroutineContextInterceptor.kt b/stub/src/main/java/io/grpc/kotlin/MetadataCoroutineContextInterceptor.kt index 768960d6..e90d91bf 100644 --- a/stub/src/main/java/io/grpc/kotlin/MetadataCoroutineContextInterceptor.kt +++ b/stub/src/main/java/io/grpc/kotlin/MetadataCoroutineContextInterceptor.kt @@ -5,31 +5,29 @@ import io.grpc.ServerCall import kotlin.coroutines.CoroutineContext /** - * A server interceptor which propagates gRPC Metadata (HTTP Headers) to coroutineContext. - * To use it attach the interceptor to gRPC Server and then access the Metadata using grpcMetadata() function. + * A server interceptor which propagates gRPC Metadata (HTTP Headers) to coroutineContext. To use it + * attach the interceptor to gRPC Server and then access the Metadata using grpcMetadata() function. * * Example usage: * - * ServerBuilder.forPort(8060) - * .addService(GreeterImpl()) - * .intercept(MetadataCoroutineContextInterceptor()) + * ServerBuilder.forPort(8060) .addService(GreeterImpl()) + * .intercept(MetadataCoroutineContextInterceptor()) * * grpcMetadata() */ class MetadataCoroutineContextInterceptor : CoroutineContextServerInterceptor() { - final override fun coroutineContext(call: ServerCall<*, *>, headers: Metadata): CoroutineContext { - return MetadataElement(value = headers) - } + final override fun coroutineContext(call: ServerCall<*, *>, headers: Metadata): CoroutineContext { + return MetadataElement(value = headers) + } } /** - * A metadata element for coroutine context. - * It is used for accessing the gRPC Metadata from coroutineContext. - * Example usage: - * coroutineContext[MetadataElement]?.value + * A metadata element for coroutine context. It is used for accessing the gRPC Metadata from + * coroutineContext. Example usage: coroutineContext[MetadataElement]?.value */ internal data class MetadataElement(val value: Metadata) : CoroutineContext.Element { - companion object Key : CoroutineContext.Key + companion object Key : CoroutineContext.Key - override val key: CoroutineContext.Key get() = Key + override val key: CoroutineContext.Key + get() = Key } diff --git a/stub/src/test/java/io/grpc/kotlin/MetadataCoroutineContextInterceptorTest.kt b/stub/src/test/java/io/grpc/kotlin/MetadataCoroutineContextInterceptorTest.kt index 43ffea11..2ad15a42 100644 --- a/stub/src/test/java/io/grpc/kotlin/MetadataCoroutineContextInterceptorTest.kt +++ b/stub/src/test/java/io/grpc/kotlin/MetadataCoroutineContextInterceptorTest.kt @@ -1,5 +1,6 @@ package io.grpc.kotlin +import com.google.common.truth.Truth.assertThat import io.grpc.BindableService import io.grpc.Channel import io.grpc.Metadata @@ -17,65 +18,71 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 -import com.google.common.truth.Truth.assertThat @RunWith(JUnit4::class) class MetadataCoroutineContextInterceptorTest { - @Rule - @JvmField - val grpcCleanup = GrpcCleanupRule() + @Rule @JvmField val grpcCleanup = GrpcCleanupRule() - @Test - fun `interceptor provides gRPC Metadata to coroutineContext`() { - val key = Metadata.Key.of("test-header", Metadata.ASCII_STRING_MARSHALLER) - val clientStub = - GreeterGrpcKt.GreeterCoroutineStub(testChannel(object : GreeterGrpcKt.GreeterCoroutineImplBase() { - override suspend fun sayHello(request: HelloRequest): HelloReply { - val metadata = grpcMetadata() - return HelloReply.newBuilder() - .setMessage(metadata.get(key).toString()) - .build() - } - })) - val metadata = Metadata() - metadata.put(key, "Test message") + @Test + fun `interceptor provides gRPC Metadata to coroutineContext`() { + val key = Metadata.Key.of("test-header", Metadata.ASCII_STRING_MARSHALLER) + val clientStub = + GreeterGrpcKt.GreeterCoroutineStub( + testChannel( + object : GreeterGrpcKt.GreeterCoroutineImplBase() { + override suspend fun sayHello(request: HelloRequest): HelloReply { + val metadata = grpcMetadata() + return HelloReply.newBuilder().setMessage(metadata.get(key).toString()).build() + } + } + ) + ) + val metadata = Metadata() + metadata.put(key, "Test message") - val response = runBlocking { clientStub.sayHello(HelloRequest.getDefaultInstance(), metadata) } + val response = runBlocking { clientStub.sayHello(HelloRequest.getDefaultInstance(), metadata) } - assertThat(response.message).isEqualTo("Test message") - } + assertThat(response.message).isEqualTo("Test message") + } - @Test - fun `fails to extract gRPC Metadata if interceptor is not injected`() { - val key = Metadata.Key.of("test-header", Metadata.ASCII_STRING_MARSHALLER) - val clientStub = - GreeterGrpcKt.GreeterCoroutineStub(testChannel(object : GreeterGrpcKt.GreeterCoroutineImplBase() { - override suspend fun sayHello(request: HelloRequest): HelloReply { - val metadata = grpcMetadata() - return HelloReply.newBuilder() - .setMessage(metadata.get(key).toString()) - .build() - } - }, false)) - val metadata = Metadata() - metadata.put(key, "Test message") - - val exception = assertFailsWith { - runBlocking { - clientStub.sayHello(HelloRequest.getDefaultInstance(), metadata) + @Test + fun `fails to extract gRPC Metadata if interceptor is not injected`() { + val key = Metadata.Key.of("test-header", Metadata.ASCII_STRING_MARSHALLER) + val clientStub = + GreeterGrpcKt.GreeterCoroutineStub( + testChannel( + object : GreeterGrpcKt.GreeterCoroutineImplBase() { + override suspend fun sayHello(request: HelloRequest): HelloReply { + val metadata = grpcMetadata() + return HelloReply.newBuilder().setMessage(metadata.get(key).toString()).build() } - } - assertThat(exception.status.code).isEqualTo(Status.INTERNAL.code) - assertThat("gRPC Metadata not found in coroutineContext. Ensure that MetadataCoroutineContextInterceptor is used in gRPC server.").isEqualTo(exception.status.description) - } + }, + false + ) + ) + val metadata = Metadata() + metadata.put(key, "Test message") + + val exception = + assertFailsWith { + runBlocking { clientStub.sayHello(HelloRequest.getDefaultInstance(), metadata) } + } + assertThat(exception.status.code).isEqualTo(Status.INTERNAL.code) + assertThat( + "gRPC Metadata not found in coroutineContext. Ensure that MetadataCoroutineContextInterceptor is used in gRPC server." + ) + .isEqualTo(exception.status.description) + } - private fun testChannel(service: BindableService, attachInterceptor: Boolean = true): Channel { - val serverName = InProcessServerBuilder.generateName() - var builder = InProcessServerBuilder.forName(serverName).directExecutor() - if (attachInterceptor) { - builder = builder.intercept(MetadataCoroutineContextInterceptor()) - } - grpcCleanup.register(builder.addService(service).build().start()) - return grpcCleanup.register(InProcessChannelBuilder.forName(serverName).directExecutor().build()) + private fun testChannel(service: BindableService, attachInterceptor: Boolean = true): Channel { + val serverName = InProcessServerBuilder.generateName() + var builder = InProcessServerBuilder.forName(serverName).directExecutor() + if (attachInterceptor) { + builder = builder.intercept(MetadataCoroutineContextInterceptor()) } + grpcCleanup.register(builder.addService(service).build().start()) + return grpcCleanup.register( + InProcessChannelBuilder.forName(serverName).directExecutor().build() + ) + } } From 9b00c324eceffae888dfb8a11287b3779235de49 Mon Sep 17 00:00:00 2001 From: Zhumazhenis Date: Mon, 22 Sep 2025 02:35:42 +0100 Subject: [PATCH 06/10] Use kt-proto. --- gradle/libs.versions.toml | 1 + stub/build.gradle.kts | 5 +++++ .../kotlin/MetadataCoroutineContextInterceptorTest.kt | 10 ++++++---- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 79d88f0c..4e0aebbc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,6 +31,7 @@ junit-jupiter-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engi mockito-core = { group = "org.mockito", name = "mockito-core", version = "5.17.0" } protoc = { group = "com.google.protobuf", name = "protoc", version = "3.25.3" } protobuf-java = { group = "com.google.protobuf", name = "protobuf-java", version = "3.25.3" } +protobuf-kotlin = { group = "com.google.protobuf", name = "protobuf-kotlin", version = "3.25.3" } truth = { group = "com.google.truth", name = "truth", version = "1.4.4" } truth-proto-extension = { group = "com.google.truth.extensions", name = "truth-proto-extension", version = "1.4.4" } okhttp = { group = "com.squareup.okhttp", name = "okhttp", version = "2.7.5" } diff --git a/stub/build.gradle.kts b/stub/build.gradle.kts index 99a58681..e12b2a12 100644 --- a/stub/build.gradle.kts +++ b/stub/build.gradle.kts @@ -29,6 +29,7 @@ dependencies { testImplementation(libs.grpc.protobuf) testImplementation(libs.grpc.testing) testImplementation(libs.grpc.inprocess) + testImplementation(libs.protobuf.kotlin) } java { @@ -54,6 +55,10 @@ protobuf { id("grpc") id("grpckt") } + + it.builtins { + id("kotlin") + } } } } diff --git a/stub/src/test/java/io/grpc/kotlin/MetadataCoroutineContextInterceptorTest.kt b/stub/src/test/java/io/grpc/kotlin/MetadataCoroutineContextInterceptorTest.kt index 2ad15a42..4439ab75 100644 --- a/stub/src/test/java/io/grpc/kotlin/MetadataCoroutineContextInterceptorTest.kt +++ b/stub/src/test/java/io/grpc/kotlin/MetadataCoroutineContextInterceptorTest.kt @@ -9,6 +9,8 @@ import io.grpc.StatusException import io.grpc.examples.helloworld.GreeterGrpcKt import io.grpc.examples.helloworld.HelloReply import io.grpc.examples.helloworld.HelloRequest +import io.grpc.examples.helloworld.helloReply +import io.grpc.examples.helloworld.helloRequest import io.grpc.inprocess.InProcessChannelBuilder import io.grpc.inprocess.InProcessServerBuilder import io.grpc.testing.GrpcCleanupRule @@ -32,7 +34,7 @@ class MetadataCoroutineContextInterceptorTest { object : GreeterGrpcKt.GreeterCoroutineImplBase() { override suspend fun sayHello(request: HelloRequest): HelloReply { val metadata = grpcMetadata() - return HelloReply.newBuilder().setMessage(metadata.get(key).toString()).build() + return helloReply { message = metadata.get(key).toString() } } } ) @@ -40,7 +42,7 @@ class MetadataCoroutineContextInterceptorTest { val metadata = Metadata() metadata.put(key, "Test message") - val response = runBlocking { clientStub.sayHello(HelloRequest.getDefaultInstance(), metadata) } + val response = runBlocking { clientStub.sayHello(helloRequest {}, metadata) } assertThat(response.message).isEqualTo("Test message") } @@ -54,7 +56,7 @@ class MetadataCoroutineContextInterceptorTest { object : GreeterGrpcKt.GreeterCoroutineImplBase() { override suspend fun sayHello(request: HelloRequest): HelloReply { val metadata = grpcMetadata() - return HelloReply.newBuilder().setMessage(metadata.get(key).toString()).build() + return helloReply { message = metadata.get(key).toString() } } }, false @@ -65,7 +67,7 @@ class MetadataCoroutineContextInterceptorTest { val exception = assertFailsWith { - runBlocking { clientStub.sayHello(HelloRequest.getDefaultInstance(), metadata) } + runBlocking { clientStub.sayHello(helloRequest {}, metadata) } } assertThat(exception.status.code).isEqualTo(Status.INTERNAL.code) assertThat( From d2f4a00aa5d2df4779319f7076e0aed89fb6a422 Mon Sep 17 00:00:00 2001 From: Zhumazhenis Date: Mon, 22 Sep 2025 23:05:05 +0100 Subject: [PATCH 07/10] Refactor test. --- .../io/grpc/kotlin/MetadataCoroutineContextInterceptorTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stub/src/test/java/io/grpc/kotlin/MetadataCoroutineContextInterceptorTest.kt b/stub/src/test/java/io/grpc/kotlin/MetadataCoroutineContextInterceptorTest.kt index 4439ab75..01967c74 100644 --- a/stub/src/test/java/io/grpc/kotlin/MetadataCoroutineContextInterceptorTest.kt +++ b/stub/src/test/java/io/grpc/kotlin/MetadataCoroutineContextInterceptorTest.kt @@ -70,10 +70,10 @@ class MetadataCoroutineContextInterceptorTest { runBlocking { clientStub.sayHello(helloRequest {}, metadata) } } assertThat(exception.status.code).isEqualTo(Status.INTERNAL.code) - assertThat( + assertThat(exception.status.description) + .isEqualTo( "gRPC Metadata not found in coroutineContext. Ensure that MetadataCoroutineContextInterceptor is used in gRPC server." ) - .isEqualTo(exception.status.description) } private fun testChannel(service: BindableService, attachInterceptor: Boolean = true): Channel { From 8c8f2156093073e3678d68e8d9fb78fa1155cd2d Mon Sep 17 00:00:00 2001 From: Zhumazhenis Date: Mon, 22 Sep 2025 23:33:18 +0100 Subject: [PATCH 08/10] Refactor doc. --- stub/src/main/java/io/grpc/kotlin/Helpers.kt | 5 ++++- .../MetadataCoroutineContextInterceptor.kt | 16 ++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/stub/src/main/java/io/grpc/kotlin/Helpers.kt b/stub/src/main/java/io/grpc/kotlin/Helpers.kt index a9fe3468..cdf072bf 100644 --- a/stub/src/main/java/io/grpc/kotlin/Helpers.kt +++ b/stub/src/main/java/io/grpc/kotlin/Helpers.kt @@ -78,7 +78,10 @@ internal fun Flow.singleOrStatusFlow(expected: String, descriptor: Any): internal suspend fun Flow.singleOrStatus(expected: String, descriptor: Any): T = singleOrStatusFlow(expected, descriptor).single() -/** Returns gRPC Metadata. */ +/** + * Returns gRPC Metadata. Throws [StatusException] if [MetadataCoroutineContextInterceptor] is not + * injected to gRPC server. + */ suspend fun grpcMetadata(): Metadata { val metadataElement = coroutineContext[MetadataElement] diff --git a/stub/src/main/java/io/grpc/kotlin/MetadataCoroutineContextInterceptor.kt b/stub/src/main/java/io/grpc/kotlin/MetadataCoroutineContextInterceptor.kt index e90d91bf..27f633bc 100644 --- a/stub/src/main/java/io/grpc/kotlin/MetadataCoroutineContextInterceptor.kt +++ b/stub/src/main/java/io/grpc/kotlin/MetadataCoroutineContextInterceptor.kt @@ -3,17 +3,19 @@ package io.grpc.kotlin import io.grpc.Metadata import io.grpc.ServerCall import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.coroutineContext /** - * A server interceptor which propagates gRPC Metadata (HTTP Headers) to coroutineContext. To use it - * attach the interceptor to gRPC Server and then access the Metadata using grpcMetadata() function. + * A server interceptor which propagates gRPC [Metadata] (HTTP Headers) to coroutineContext. To use + * it attach the interceptor to gRPC Server and then access the [Metadata] using grpcMetadata() + * function. * * Example usage: * - * ServerBuilder.forPort(8060) .addService(GreeterImpl()) + * ServerBuilder.forPort(8060).addService(GreeterImpl()) * .intercept(MetadataCoroutineContextInterceptor()) * - * grpcMetadata() + * Then in RPC implementation code call grpcMetadata() */ class MetadataCoroutineContextInterceptor : CoroutineContextServerInterceptor() { final override fun coroutineContext(call: ServerCall<*, *>, headers: Metadata): CoroutineContext { @@ -22,8 +24,10 @@ class MetadataCoroutineContextInterceptor : CoroutineContextServerInterceptor() } /** - * A metadata element for coroutine context. It is used for accessing the gRPC Metadata from - * coroutineContext. Example usage: coroutineContext[MetadataElement]?.value + * A metadata element for coroutine context. It is used for accessing the gRPC [Metadata] from + * [coroutineContext]. + * + * Example usage: coroutineContext[MetadataElement]?.value */ internal data class MetadataElement(val value: Metadata) : CoroutineContext.Element { companion object Key : CoroutineContext.Key From ab378521c284dad8bdfc7af02bf8f27f4bd7d207 Mon Sep 17 00:00:00 2001 From: Zhumazhenis Date: Wed, 24 Sep 2025 23:23:07 +0100 Subject: [PATCH 09/10] Resolve comments. --- stub/src/main/java/io/grpc/kotlin/Helpers.kt | 8 +++---- .../MetadataCoroutineContextInterceptor.kt | 4 +--- ...MetadataCoroutineContextInterceptorTest.kt | 22 +++++++++++-------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/stub/src/main/java/io/grpc/kotlin/Helpers.kt b/stub/src/main/java/io/grpc/kotlin/Helpers.kt index cdf072bf..5fc9da1b 100644 --- a/stub/src/main/java/io/grpc/kotlin/Helpers.kt +++ b/stub/src/main/java/io/grpc/kotlin/Helpers.kt @@ -85,10 +85,8 @@ internal suspend fun Flow.singleOrStatus(expected: String, descriptor: An suspend fun grpcMetadata(): Metadata { val metadataElement = coroutineContext[MetadataElement] - ?: throw StatusException( - Status.INTERNAL.withDescription( - "gRPC Metadata not found in coroutineContext. Ensure that MetadataCoroutineContextInterceptor is used in gRPC server." - ) - ) + ?: throw Status.INTERNAL.withDescription( + "gRPC Metadata not found in coroutineContext. Ensure that MetadataCoroutineContextInterceptor is used in gRPC server." + ).asException() return metadataElement.value } diff --git a/stub/src/main/java/io/grpc/kotlin/MetadataCoroutineContextInterceptor.kt b/stub/src/main/java/io/grpc/kotlin/MetadataCoroutineContextInterceptor.kt index 27f633bc..5a85bd1a 100644 --- a/stub/src/main/java/io/grpc/kotlin/MetadataCoroutineContextInterceptor.kt +++ b/stub/src/main/java/io/grpc/kotlin/MetadataCoroutineContextInterceptor.kt @@ -18,9 +18,7 @@ import kotlin.coroutines.coroutineContext * Then in RPC implementation code call grpcMetadata() */ class MetadataCoroutineContextInterceptor : CoroutineContextServerInterceptor() { - final override fun coroutineContext(call: ServerCall<*, *>, headers: Metadata): CoroutineContext { - return MetadataElement(value = headers) - } + final override fun coroutineContext(call: ServerCall<*, *>, headers: Metadata): CoroutineContext = MetadataElement(value = headers) } /** diff --git a/stub/src/test/java/io/grpc/kotlin/MetadataCoroutineContextInterceptorTest.kt b/stub/src/test/java/io/grpc/kotlin/MetadataCoroutineContextInterceptorTest.kt index 01967c74..6298195c 100644 --- a/stub/src/test/java/io/grpc/kotlin/MetadataCoroutineContextInterceptorTest.kt +++ b/stub/src/test/java/io/grpc/kotlin/MetadataCoroutineContextInterceptorTest.kt @@ -26,44 +26,48 @@ class MetadataCoroutineContextInterceptorTest { @Rule @JvmField val grpcCleanup = GrpcCleanupRule() @Test - fun `interceptor provides gRPC Metadata to coroutineContext`() { - val key = Metadata.Key.of("test-header", Metadata.ASCII_STRING_MARSHALLER) + fun interceptCall_providesMetadataToCoroutineContext() { + val keyA = Metadata.Key.of("test-header-a", Metadata.ASCII_STRING_MARSHALLER) + val keyB = Metadata.Key.of("test-header-b", Metadata.ASCII_STRING_MARSHALLER) val clientStub = GreeterGrpcKt.GreeterCoroutineStub( testChannel( object : GreeterGrpcKt.GreeterCoroutineImplBase() { override suspend fun sayHello(request: HelloRequest): HelloReply { val metadata = grpcMetadata() - return helloReply { message = metadata.get(key).toString() } + return helloReply { message = listOf(metadata.get(keyA).toString(), metadata.get(keyB).toString()).joinToString() } } } ) ) val metadata = Metadata() - metadata.put(key, "Test message") + metadata.put(keyA, "Test message A") + metadata.put(keyB, "Test message B") val response = runBlocking { clientStub.sayHello(helloRequest {}, metadata) } - assertThat(response.message).isEqualTo("Test message") + assertThat(response.message).isEqualTo("Test message A, Test message B") } @Test - fun `fails to extract gRPC Metadata if interceptor is not injected`() { - val key = Metadata.Key.of("test-header", Metadata.ASCII_STRING_MARSHALLER) + fun grpcMetadata_interceptorNotInjected_throwsStatusExceptionInternal() { + val keyA = Metadata.Key.of("test-header-a", Metadata.ASCII_STRING_MARSHALLER) + val keyB = Metadata.Key.of("test-header-b", Metadata.ASCII_STRING_MARSHALLER) val clientStub = GreeterGrpcKt.GreeterCoroutineStub( testChannel( object : GreeterGrpcKt.GreeterCoroutineImplBase() { override suspend fun sayHello(request: HelloRequest): HelloReply { val metadata = grpcMetadata() - return helloReply { message = metadata.get(key).toString() } + return helloReply { message = listOf(metadata.get(keyA).toString(), metadata.get(keyB).toString()).joinToString() } } }, false ) ) val metadata = Metadata() - metadata.put(key, "Test message") + metadata.put(keyA, "Test message A") + metadata.put(keyB, "Test message B") val exception = assertFailsWith { From d59230c202ce62532e3f7555271e6c901ccf321f Mon Sep 17 00:00:00 2001 From: Zhumazhenis Date: Wed, 24 Sep 2025 23:25:52 +0100 Subject: [PATCH 10/10] Format. --- stub/src/main/java/io/grpc/kotlin/Helpers.kt | 5 +++-- .../io/grpc/kotlin/MetadataCoroutineContextInterceptor.kt | 3 ++- .../kotlin/MetadataCoroutineContextInterceptorTest.kt | 8 ++++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/stub/src/main/java/io/grpc/kotlin/Helpers.kt b/stub/src/main/java/io/grpc/kotlin/Helpers.kt index 5fc9da1b..511ce4de 100644 --- a/stub/src/main/java/io/grpc/kotlin/Helpers.kt +++ b/stub/src/main/java/io/grpc/kotlin/Helpers.kt @@ -86,7 +86,8 @@ suspend fun grpcMetadata(): Metadata { val metadataElement = coroutineContext[MetadataElement] ?: throw Status.INTERNAL.withDescription( - "gRPC Metadata not found in coroutineContext. Ensure that MetadataCoroutineContextInterceptor is used in gRPC server." - ).asException() + "gRPC Metadata not found in coroutineContext. Ensure that MetadataCoroutineContextInterceptor is used in gRPC server." + ) + .asException() return metadataElement.value } diff --git a/stub/src/main/java/io/grpc/kotlin/MetadataCoroutineContextInterceptor.kt b/stub/src/main/java/io/grpc/kotlin/MetadataCoroutineContextInterceptor.kt index 5a85bd1a..24e3e03d 100644 --- a/stub/src/main/java/io/grpc/kotlin/MetadataCoroutineContextInterceptor.kt +++ b/stub/src/main/java/io/grpc/kotlin/MetadataCoroutineContextInterceptor.kt @@ -18,7 +18,8 @@ import kotlin.coroutines.coroutineContext * Then in RPC implementation code call grpcMetadata() */ class MetadataCoroutineContextInterceptor : CoroutineContextServerInterceptor() { - final override fun coroutineContext(call: ServerCall<*, *>, headers: Metadata): CoroutineContext = MetadataElement(value = headers) + final override fun coroutineContext(call: ServerCall<*, *>, headers: Metadata): CoroutineContext = + MetadataElement(value = headers) } /** diff --git a/stub/src/test/java/io/grpc/kotlin/MetadataCoroutineContextInterceptorTest.kt b/stub/src/test/java/io/grpc/kotlin/MetadataCoroutineContextInterceptorTest.kt index 6298195c..deaddb79 100644 --- a/stub/src/test/java/io/grpc/kotlin/MetadataCoroutineContextInterceptorTest.kt +++ b/stub/src/test/java/io/grpc/kotlin/MetadataCoroutineContextInterceptorTest.kt @@ -35,7 +35,9 @@ class MetadataCoroutineContextInterceptorTest { object : GreeterGrpcKt.GreeterCoroutineImplBase() { override suspend fun sayHello(request: HelloRequest): HelloReply { val metadata = grpcMetadata() - return helloReply { message = listOf(metadata.get(keyA).toString(), metadata.get(keyB).toString()).joinToString() } + return helloReply { + message = listOf(metadata.get(keyA), metadata.get(keyB)).joinToString() + } } } ) @@ -59,7 +61,9 @@ class MetadataCoroutineContextInterceptorTest { object : GreeterGrpcKt.GreeterCoroutineImplBase() { override suspend fun sayHello(request: HelloRequest): HelloReply { val metadata = grpcMetadata() - return helloReply { message = listOf(metadata.get(keyA).toString(), metadata.get(keyB).toString()).joinToString() } + return helloReply { + message = listOf(metadata.get(keyA), metadata.get(keyB)).joinToString() + } } }, false