-
Notifications
You must be signed in to change notification settings - Fork 17
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
Support empty buffers on Gzip decompression #139
Support empty buffers on Gzip decompression #139
Conversation
3532c77
to
c6e1c36
Compare
setBody(Buffer()) | ||
setResponseCode(401) |
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 isn't quite representative of the original scenario we were seeing: the server was returning a non-empty response, but by the time it got here, the response message buffer was already empty.
Our hypothesis after stepping through the code a few times, was that parseConnectUnaryException()
was consuming the message buffer before the decompress call had a change to consume it: the message was always non-empty when parseConnectUnaryException
was called. We thought the root cause might be to decompress the response first, then attempt to parse it into an error message. I haven't been able to reproduce that exact scenario in this test, though; the actual root cause may still be TBD.
okhttp/build.gradle.kts
Outdated
testImplementation(libs.okhttp.mockwebserver) | ||
testImplementation(libs.kotlin.coroutines.test) | ||
testImplementation(project(":extensions:google-java")) | ||
testImplementation(project(":conformance:google-java")) |
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.
I don't think we want the okhttp module to depend on the conformance module - perhaps we just need a simple Eliza codegen in okhttp for tests so we have a service we can use.
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.
Sure, that makes sense. Would that be set up similarly to how the :examples:generated-google-java
and :examples:generated-google-javalite
modules are configured?
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.
I think depending on :examples:google-java
wouldn't be too bad and that way we wouldn't have to generate those files again.
|
||
@Test | ||
fun `compressed empty failure response is parsed correctly`() = runTest { | ||
val mockWebServer = MockWebServer() |
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 should wire up https://github.com/square/okhttp/tree/master/mockwebserver-junit4 (or even better migrate these tests to junit jupiter and use https://github.com/square/okhttp/tree/master/mockwebserver-junit5. This will make it easier over time to add more tests here without worrying about construction/start/shutdown being called in each.
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.
Great idea. Do you have a preference over JUnit 4 vs. 5?
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.
My preference would be to use JUnit 5 for new tests but if that complicates things we can do a migration of all the tests in the future in a separate PR.
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.
Ended up using JUnit4. I think it makes sense to migrate everything over to JUnit Jupiter in a future PR.
) | ||
|
||
val request = simpleRequest {} | ||
TestServiceClient(protocolClient).unaryCall(request) |
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.
Don't we want to assert something here about the returned call (that it has an appropriate connect status 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.
Yep, I can add some more detailed assertions here
67eac59
to
db8ef1d
Compare
gradle/libs.versions.toml
Outdated
@@ -9,6 +9,7 @@ kotlinpoet = "1.14.2" | |||
mavenplugin = "0.25.3" | |||
moshi = "1.15.0" | |||
okhttp = "4.12.0" | |||
okhttp-junit = "5.0.0-alpha.11" |
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.
lemme know if you'd rather keep the okhttp versions the same (and depend on the older mockwebserver
artifact)
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.
Yeah let's keep them in sync.
Gzip decompression fails when empty error responses are returned
affef99
to
5e28851
Compare
f63fcfa
to
ff93b51
Compare
@@ -38,6 +38,8 @@ object GzipCompressionPool : CompressionPool { | |||
|
|||
override fun decompress(buffer: Buffer): Buffer { | |||
val result = Buffer() | |||
if (buffer.size == 0L) return result |
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 makes sense and is a good change (and we've amended the spec to account for it):
Servers must not attempt to decompress zero-length HTTP request content.
I think in this specific error case though, there was another issue. In
connect-kotlin/library/src/main/kotlin/com/connectrpc/protocols/ConnectInterceptor.kt
Lines 98 to 104 in 37fb4f9
val (code, exception) = if (response.code != Code.OK) { | |
val error = parseConnectUnaryException(code = response.code, response.headers, response.message.buffer) | |
error.code to error | |
} else { | |
response.code to null | |
} | |
val message = compressionPool?.decompress(response.message.buffer) ?: response.message.buffer |
parseConnectUnaryException
, and then on line 104 we're again trying to consume the same message body a second time. If the code is not OK, we shouldn't be attempting to consume a response message and instead should just set message to an empty buffer.
assertThat(path).isEqualTo("/connectrpc.eliza.v1.ElizaService/Say") | ||
} | ||
|
||
assertThat(response.code).isEqualTo(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.
Wrote a small connect-go example and verified that it gets the same result here. Thanks for adding these tests - they'll make it much easier to test different edge conditions outside of the conformance tests in the future.
Fixes #138
Gzipped responses that return an empty message Buffer fail with an
EOFException
, halting the request chain.The fix is relatively simple, but I'm not sure if this is actually a valid state connect-kotlin should support