Skip to content
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

Merged
merged 8 commits into from
Sep 19, 2023
Merged

Conversation

pkwarren
Copy link
Contributor

@pkwarren pkwarren commented Sep 15, 2023

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 sending TE: 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).

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.
@pkwarren pkwarren requested review from rebello95 and removed request for akshayjshah September 15, 2023 21:04
arrayOf(NetworkProtocol.GRPC, NetworkProtocol.GRPC)
arrayOf(NetworkProtocol.CONNECT, ServerType.CONNECT_GO),
arrayOf(NetworkProtocol.GRPC, ServerType.CONNECT_GO),
arrayOf(NetworkProtocol.GRPC_WEB, ServerType.CONNECT_GO),
Copy link
Contributor Author

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 {
Copy link
Contributor Author

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()
Copy link
Contributor Author

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")
Copy link
Contributor Author

@pkwarren pkwarren Sep 19, 2023

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.

Copy link
Contributor

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") }
Copy link
Contributor Author

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)
Copy link
Contributor

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

Copy link
Contributor Author

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 {
Copy link
Contributor

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?

Copy link
Contributor Author

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 {
Copy link
Contributor

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question about trying here

)

internal fun grpcCompletionToConnectError(completion: GRPCCompletion?, serializationStrategy: SerializationStrategy, error: Throwable?): ConnectError? {
val code = completion?.code ?: Code.UNKNOWN
Copy link
Contributor

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.
Copy link
Contributor

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,
Copy link
Contributor

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?

Copy link
Contributor Author

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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rebello95
Copy link
Contributor

Also, can you please update the PR title/description?

@pkwarren pkwarren merged commit 2fd1600 into main Sep 19, 2023
9 checks passed
@pkwarren pkwarren deleted the pkw/grpc-trailers-only branch September 19, 2023 16:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants