-
Notifications
You must be signed in to change notification settings - Fork 15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix handling of gRPC trailers-only responses #101
Conversation
connect-kotlin wasn't properly handling gRPC trailers-only responses (which are actually responses with *no* trailers, just headers), leading to the inability to read grpc-status headers properly. Update the connect-kotlin conformance tests to run combinations of both Connect/gRPC client and server instead of just Connect servers. By enabling this earlier, we would've detected the trailers-only issue earlier with the conformance test.
conformance/google-java/src/test/kotlin/com/connectrpc/conformance/Conformance.kt
Outdated
Show resolved
Hide resolved
conformance/google-java/src/test/kotlin/com/connectrpc/conformance/Conformance.kt
Show resolved
Hide resolved
conformance/google-java/src/test/kotlin/com/connectrpc/conformance/Conformance.kt
Show resolved
Hide resolved
library/src/main/kotlin/com/connectrpc/protocols/ConnectInterceptor.kt
Outdated
Show resolved
Hide resolved
library/src/main/kotlin/com/connectrpc/protocols/GRPCInterceptor.kt
Outdated
Show resolved
Hide resolved
conformance/google-java/src/test/kotlin/com/connectrpc/conformance/Conformance.kt
Outdated
Show resolved
Hide resolved
library/src/main/kotlin/com/connectrpc/protocols/GRPCInterceptor.kt
Outdated
Show resolved
Hide resolved
library/src/main/kotlin/com/connectrpc/protocols/GRPCInterceptor.kt
Outdated
Show resolved
Hide resolved
arrayOf(NetworkProtocol.GRPC, NetworkProtocol.GRPC) | ||
arrayOf(NetworkProtocol.CONNECT, ServerType.CONNECT_GO), | ||
arrayOf(NetworkProtocol.GRPC, ServerType.CONNECT_GO), | ||
arrayOf(NetworkProtocol.GRPC_WEB, ServerType.CONNECT_GO), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added gRPC web protocol tests as well (and found some issues as a result).
assertThat(response.code).isEqualTo(Code.UNIMPLEMENTED) | ||
countDownLatch.countDown() | ||
} | ||
countDownLatch.await(500, TimeUnit.MILLISECONDS) | ||
assertThat(countDownLatch.count).isZero() | ||
} | ||
|
||
@Test | ||
fun unimplementedServerStreamingService(): Unit = runBlocking { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added this test which caught the streaming behavior issue: #101 (comment)
@@ -276,9 +246,9 @@ internal class GRPCWebInterceptor( | |||
} | |||
val i = line.indexOf(":") | |||
if (i > 0) { | |||
val name = line.substring(0, i).trim().lowercase() | |||
val value = line.substring(i + 1).trim().lowercase() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This caused issues parsing error details in gRPC-Web (failing conformance tests). We were setting a base64 encoded protobuf to lowercase, which ended up reading as an invalid proto message on the other end.
@@ -259,7 +230,6 @@ internal class GRPCWebInterceptor( | |||
if (headers.keys.none { it.equals(GRPC_WEB_USER_AGENT, ignoreCase = true) }) { | |||
headers[GRPC_WEB_USER_AGENT] = listOf("grpc-kotlin-connect/${ConnectConstants.VERSION}") | |||
} | |||
headers[GRPC_TE_HEADER] = listOf("trailers") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
gRPC-web specifically doesn't support trailers - we shouldn't send this header.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -188,26 +185,14 @@ internal class GRPCWebInterceptor( | |||
streamResultFunction = { res -> | |||
val streamResult = res.fold( | |||
onHeaders = { result -> | |||
val responseHeaders = result.headers.filter { entry -> !entry.key.startsWith("trailer") } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only Connect, not the gRPC* protocols have this convention with trailers and the trailer-
prefix.
} | ||
|
||
@Before | ||
fun before() { | ||
val host = "https://localhost:${CONFORMANCE_CONTAINER.getMappedPort(8081)}" | ||
val serverPort = if (serverType == ServerType.CONNECT_GO) CONFORMANCE_CONTAINER_CONNECT.getMappedPort(8081) else CONFORMANCE_CONTAINER_GRPC.getMappedPort(8081) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a big deal, but calling out that Swift uses 8083 for grpc-go because that's where it's hosted in the container: https://github.com/connectrpc/connect-swift/blob/f7aab0e53c38f15d5ab62e8e8afa2f984c7e34f3/Tests/ConnectLibraryTests/ConnectConformance/ConformanceConfiguration.swift#L58
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could update to match, however that is coming from https://github.com/connectrpc/connect-swift/blob/f7aab0e53c38f15d5ab62e8e8afa2f984c7e34f3/Makefile#L49-L54. We're no longer needing to spawn containers from the Makefile in this project - we're using testcontainers to do that, so port 8081 is actually ephemeral on the host.
expectedErrorDetail | ||
) | ||
countDownLatch.countDown() | ||
try { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't we want this to explode if any of the asserts fail rather than allowing tests to continue?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will still fail the test, but this will prevent it waiting 5 seconds on the countDownLatch for the failure to register. Verified by adding a failing assert.
assertThat(result.connectError()!!.code).isEqualTo(Code.DEADLINE_EXCEEDED) | ||
assertThat(result.code).isEqualTo(Code.DEADLINE_EXCEEDED) | ||
countDownLatch.countDown() | ||
try { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here
for (res in stream.resultChannel()) { | ||
res.maybeFold( | ||
onCompletion = { result -> | ||
try { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same question about try
ing here
) | ||
|
||
internal fun grpcCompletionToConnectError(completion: GRPCCompletion?, serializationStrategy: SerializationStrategy, error: Throwable?): ConnectError? { | ||
val code = completion?.code ?: Code.UNKNOWN |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could move this down to after the error check since it's not used immediately
@@ -27,24 +28,34 @@ class GRPCCompletionParser( | |||
private val errorDetailParser: ErrorDetailParser | |||
) { | |||
/** | |||
* Parses the completion of a GRPC response from the Trailers. | |||
* Parses the completion of a GRPC response from the Headers (for trailers-only responses) or Trailers. | |||
* | |||
* For GRPCWeb, the caller will have to transform the final message into trailers to parse a completion. | |||
* | |||
* For GRPC H2, the caller can just take the trailers from the completed stream to parse a completion. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably be updated
if (completion != null) { | ||
val connectError = grpcCompletionToConnectError(completion, serializationStrategy, result.error) | ||
return@fold StreamResult.Complete( | ||
code = connectError?.code ?: Code.OK, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be completion!!.code
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No - in some cases this is a ConnectError (error is ConnectError
) which has its own code which takes precedence. This is important in some of the tests that exercise deadlines.
@@ -259,7 +230,6 @@ internal class GRPCWebInterceptor( | |||
if (headers.keys.none { it.equals(GRPC_WEB_USER_AGENT, ignoreCase = true) }) { | |||
headers[GRPC_WEB_USER_AGENT] = listOf("grpc-kotlin-connect/${ConnectConstants.VERSION}") | |||
} | |||
headers[GRPC_TE_HEADER] = listOf("trailers") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, can you please update the PR title/description? |
connect-kotlin wasn't properly handling gRPC trailers-only responses, leading to the inability to read grpc-status headers properly. Update the GRPCCompletionParser to first look for
grpc-status
in headers then trailers to handle these cases.Update the connect-kotlin conformance tests to run combinations of both Connect/gRPC/gRPC-Web protocols and Connect/gRPC servers instead of just Connect/gRPC against a Connect server (which always sends trailers). By enabling this earlier, we would've detected the trailers-only issue earlier with the conformance test.
Fix gRPC protocol handlers to not filter out headers with
trailer-
prefix - this should only happen for the Connect protocol. Stop sendingTE: trailers
on gRPC-web requests (they don't use trailers). Preserve header value case in gRPC-web (leading to errors parsing base64-encoded error details).