feat: add gRPC interceptor support as library-grpc module#1631
feat: add gRPC interceptor support as library-grpc module#1631MRezaNasirloo wants to merge 3 commits into
Conversation
Adds ChuckerGrpcInterceptor to library and library-no-op so gRPC calls
are visible alongside HTTP traffic in the Chucker UI.
Usage:
val channel = OkHttpChannelBuilder.forAddress(host, port)
.intercept(ChuckerGrpcInterceptor(collector, context))
.build()
Swap library-no-op's no-op variant for release builds — constructor
signatures match exactly for source-level compatibility.
Supported call types: unary, server-streaming, client-streaming,
bidirectional streaming. Streaming responses update live as messages
arrive rather than waiting for the stream to close.
Features:
- Request/response headers (with optional redaction)
- Body capture up to maxContentLength (default 250 KB), truncated if exceeded
- gRPC status code and description
- Response trailers shown alongside response headers
- Duration tracking
grpc-api is a compileOnly dependency; users who integrate gRPC already
have it on their classpath, and users who don't are unaffected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5b3cc14 to
c0d636e
Compare
protobuf-gradle-plugin 0.9.x Kotlin DSL requires create() not id();
the id() extension needs an import that was unavailable after cleanup.
Also drops redundant id("java") — Android handles javalite automatically.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wire owns src/main/proto (pokemon.proto → Kotlin via Wire runtime).
Protobuf-gradle-plugin must not overlap that directory or both tools
generate classes in the same package, causing duplicate-class errors.
- Move sample_service.proto to src/main/grpc (protobuf-plugin's dir)
- Redirect protobuf-plugin Android source set proto dirs to src/main/grpc
via configure<AppExtension> so Wire never sees the gRPC proto
- Add builtins { java } to generate HelloRequest/HelloReply message classes
- Add grpc-protobuf dep (ProtoUtils marshaller used by generated stubs)
- Add javax.annotation-api (compileOnly) — @generated removed from JDK 9+
- Exclude protobuf-javalite globally: play-services-cronet pulls in an old
version that conflicts with protobuf-java (which is a superset)
- Fix detekt: extract magic numbers to constants, narrow exception types,
suppress LongParameterList on Composables (known Compose pattern)
- Fix ktlint style violations
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Before sending a PR like this, why haven't you opened a GH issue to discuss it? |
There was a problem hiding this comment.
Pull request overview
Adds gRPC traffic capture to Chucker by introducing a ChuckerGrpcInterceptor (with a no-op counterpart) and a sample app demo that exercises all four gRPC call types, so gRPC calls can be inspected alongside HTTP transactions in the Chucker UI.
Changes:
- Added
ChuckerGrpcInterceptor(real) and a source-compatible no-opChuckerGrpcInterceptorimplementation. - Extended the sample app with an embedded gRPC server + client task and UI controls to trigger gRPC calls.
- Updated Gradle version catalog and sample build configuration to generate gRPC stubs via protobuf/protoc.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| sample/src/main/res/values/strings.xml | Adds UI string for the new gRPC demo action. |
| sample/src/main/kotlin/com/chuckerteam/chucker/sample/SampleGrpcServer.kt | Adds an embedded gRPC server implementing unary + streaming call types. |
| sample/src/main/kotlin/com/chuckerteam/chucker/sample/SampleApplication.kt | Starts/stops the embedded gRPC server and exposes a ChuckerCollector. |
| sample/src/main/kotlin/com/chuckerteam/chucker/sample/MainActivity.kt | Wires a new “Do gRPC activity” action into the UI and manages task lifecycle. |
| sample/src/main/kotlin/com/chuckerteam/chucker/sample/GrpcTask.kt | Adds a demo gRPC client that makes unary/streaming calls through ChuckerGrpcInterceptor. |
| sample/src/main/kotlin/com/chuckerteam/chucker/sample/compose/testtags/ChuckerTestTags.kt | Adds a test tag for the new gRPC button. |
| sample/src/main/kotlin/com/chuckerteam/chucker/sample/compose/ChuckerSampleMainScreen.kt | Adds onDoGrpc callback plumbing to the main Compose screen. |
| sample/src/main/kotlin/com/chuckerteam/chucker/sample/compose/ChuckerSampleControls.kt | Adds a gRPC button to the control list and tags. |
| sample/src/main/grpc/sample_service.proto | Defines the gRPC service and messages used by the sample. |
| sample/src/main/AndroidManifest.xml | Registers SampleApplication. |
| sample/build.gradle.kts | Adds protobuf + gRPC dependencies and configures protoc/grpc codegen for the sample. |
| library/src/main/kotlin/com/chuckerteam/chucker/api/ChuckerGrpcInterceptor.kt | Implements the gRPC ClientInterceptor that records gRPC activity into HttpTransaction. |
| library/build.gradle.kts | Adds (compileOnly) gRPC API dependency for the new interceptor. |
| library/api/library.api | Updates the public API dump to include ChuckerGrpcInterceptor. |
| library-no-op/src/main/kotlin/com/chuckerteam/chucker/api/ChuckerGrpcInterceptor.kt | Adds the no-op pass-through interceptor variant. |
| library-no-op/build.gradle.kts | Adds (compileOnly) gRPC API dependency for the no-op interceptor. |
| library-no-op/api/library-no-op.api | Updates the no-op public API dump to include ChuckerGrpcInterceptor. |
| gradle/libs.versions.toml | Adds gRPC/protobuf versions and library/plugin coordinates. |
| CHANGELOG.md | Documents the new ChuckerGrpcInterceptor feature. |
Comments suppressed due to low confidence (1)
library/src/main/kotlin/com/chuckerteam/chucker/api/ChuckerGrpcInterceptor.kt:85
- Potential race:
collector.onRequestSent(transaction)inserts the transaction asynchronously and assignstransaction.idlater.onMessage()/onClose()callcollector.onResponseReceived(transaction)immediately afterensureStarted(), butupdateTransaction()will be a no-op until the insert finishes, so early/fast gRPC responses can be dropped (including the final response if close happens quickly). Ensure the insert completes before issuing any updates (e.g., await insertion or provide a collector API that guarantees ordering).
fun ensureStarted() {
if (transactionStarted.compareAndSet(false, true)) {
collector.onRequestSent(transaction)
}
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Wire owns src/main/proto (pokemon.proto). Redirect protobuf-plugin to src/main/grpc so the two | ||
| // tools never process the same files, avoiding duplicate-class errors (Wire→Kotlin, protoc→Java). | ||
| configure<com.android.build.gradle.AppExtension> { | ||
| sourceSets.getByName("main") { | ||
| @Suppress("UNCHECKED_CAST") | ||
| val proto = | ||
| (this as org.gradle.api.plugins.ExtensionAware) | ||
| .extensions | ||
| .getByName("proto") as org.gradle.api.file.SourceDirectorySet | ||
| proto.setSrcDirs(listOf(file("src/main/grpc"))) | ||
| } | ||
| } |
| api(libs.okhttp) | ||
| api(libs.okhttp3.okhttp) | ||
| compileOnly(libs.grpc.api) | ||
| testImplementation(libs.mockwebserver) |
| dependencies { | ||
| api(libs.okhttp) | ||
| compileOnly(libs.grpc.api) | ||
| implementation(libs.jetbrains.kotlin.stdlib) | ||
| } |
| fun execute() { | ||
| scope.launch { | ||
| channel = | ||
| OkHttpChannelBuilder | ||
| .forAddress("localhost", SampleApplication.GRPC_PORT) | ||
| .usePlaintext() | ||
| .intercept(interceptor) | ||
| .build() | ||
|
|
||
| val blocking = SampleGreeterGrpc.newBlockingStub(channel) | ||
| val async = SampleGreeterGrpc.newStub(channel) | ||
|
|
||
| runUnary(blocking) | ||
| delay(CALL_DELAY_MS) | ||
| runServerStream(blocking) | ||
| delay(CALL_DELAY_MS) | ||
| runClientStream(async) | ||
| delay(CALL_DELAY_MS) | ||
| runBidiStream(async) | ||
|
|
||
| channel.shutdown().awaitTermination(SHUTDOWN_TIMEOUT_SEC, TimeUnit.SECONDS) | ||
| } |
| val authority = next.authority() ?: "unknown" | ||
| transaction.host = authority.substringBefore(":") | ||
| transaction.path = "/${method.fullMethodName}" | ||
| transaction.scheme = if (authority.substringAfterLast(':').toIntOrNull() == HTTPS_PORT) "https" else "http" | ||
| transaction.url = "${transaction.scheme}://$authority${transaction.path}" |
| - Long-press the payload copy button to choose between copying the raw body or the formatted body [#1613] | ||
| - `ChuckerGrpcInterceptor` — a gRPC `ClientInterceptor` that captures all four gRPC call types (unary, server-streaming, client-streaming, bidirectional) and displays them alongside HTTP traffic in Chucker. Streaming responses update live as messages arrive. Add it to your channel via `OkHttpChannelBuilder.intercept(ChuckerGrpcInterceptor(collector, context))`. Swap `library-no-op`'s no-op variant for release builds. |
@cortinico Hey there! This is still a work in progress. I accidentally opened this PR. I’m reworking it to remove the new modules so users don’t have to add them manually. I’ve set it to draft, but feel free to close it if you’d like. |
Adds ChuckerGrpcInterceptor in a new optional library-grpc module so gRPC calls are visible alongside HTTP traffic in the Chucker UI. A matching library-grpc-no-op module provides the release-build no-op.
📷 Screenshots
📄 Context
📝 Changes
📎 Related PR
🚫 Breaking
🛠️ How to test
⏱️ Next steps