Skip to content

TrailersSource, a new public API for HTTP trailers #8829

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

Merged
merged 4 commits into from
Jun 2, 2025

Conversation

swankjesse
Copy link
Collaborator

This doesn't change much.

The new interface replaces a Kotlin lambda that had similar behavior, though that lambda wasn't specified as thoroughly.

We now permit calls to Response.trailers() before the entire response body is consumed. But when such calls are made the response body is discarded first. This could be a bit of a surprise to users, especially users who do an additional layer of buffering over their ResponseBody because it could yield latent failures.

But I think it's better behavior overall, especially since it's possible data binding layers might not consume the entire response body in all cases. For example, a JSON library might not read to the very end of the response if it has received a terminating '}' byte.

The riskiest part of this change is when ResponseBodySource self-reports as complete. Previously it would self-report as complete when it returned as many bytes as promised in the Content-Length. With this change it will now only report itself complete when it receives a definitive EOF on the stream, signaled by a -1 byte count on a read. This is because only when the EOF is received can we be sure that the trailers are received, and we must not unhook the Exchange from the Call until that happens. The previous behavior could make Call.cancel() no-op even if we were blocked waiting on trailers.

This doesn't change much.

The new interface replaces a Kotlin lambda that had
similar behavior, though that lambda wasn't specified
as thoroughly.

We now permit calls to Response.trailers() before the
entire response body is consumed. But when such calls are
made the response body is discarded first. This could be
a bit of a surprise to users, especially users who do an
additional layer of buffering over their ResponseBody
because it could yield latent failures.

But I think it's better behavior overall, especially since
it's possible data binding layers might not consume the
entire response body in all cases. For example, a JSON
library might not read to the very end of the response
if it has received a terminating '}' byte.

The riskiest part of this change is when ResponseBodySource
self-reports as complete. Previously it would self-report
as complete when it returned as many bytes as promised
in the Content-Length. With this change it will now only
report itself complete when it receives a definitive EOF
on the stream, signaled by a -1 byte count on a read. This
is because only when the EOF is received can we be sure
that the trailers are received, and we must not unhook
the Exchange from the Call until that happens. The previous
behavior could make Call.cancel() no-op even if we were
blocked waiting on trailers.
@@ -13,6 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress(
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Gradle is satisfied by the friends mechanism, but IntelliJ needs this also

* call this without consuming the complete response body, any remaining bytes in the response
* body will be discarded before trailers are returned.
*
* If [Call.cancel] is called while this is blocking, this call will immediately throw.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

keeping this promise was tricky!

trailersAndCache(Protocol.HTTP_2)
}

private fun trailersAndCache(protocol: Protocol) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

TOTALLY DOESN’T WORK

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

(Follow-up)


@Test
fun delayBeforeTrailersHttp1() {
delayBeforeTrailers(Protocol.HTTP_1_1)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This manual no-burst stuff is the worst. I haven’t done it here 'cause I’m afraid of how our MockWebServer JUnit 5 thing will interact with Burst.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

(More precisely, Burst is broken when you do fancy things with JUnit 5)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Is that something to fix in Burst, Junit 5, or mws-ju5?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don’t know if we can fix it anywhere. Burst kind of assumes it can do its own naughty business with parameters, and it might not have enough information to leave the JUnit-5 managed ones alone.

val trailers = response.trailers()
assertThat(trailers).isEqualTo(headersOf("t1", "v2"))
}
assertThat(trailersDelay).isGreaterThan(250.milliseconds)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Defending against weirdness like the client assuming there’s no trailers 'cause there’s no trailers yet

@swankjesse swankjesse added android-regression Run a PR against regression tests jdkversions JDK 8, 17, 19 etc. labels May 30, 2025

@Test
fun delayBeforeTrailersHttp1() {
delayBeforeTrailers(Protocol.HTTP_1_1)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is that something to fix in Burst, Junit 5, or mws-ju5?

@swankjesse
Copy link
Collaborator Author

To fix the tests I needed a to push a commit with non-trivial changes to Http1ExchangeCodec.

@yschimke Please take a second look?

One of our tests caught a situation where we were waiting
for the caller to read the EOF when we didn't need to.
@swankjesse swankjesse merged commit 6de88ec into master Jun 2, 2025
23 checks passed
@swankjesse swankjesse deleted the jwilson.0529.trailers branch June 2, 2025 14:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
android-regression Run a PR against regression tests jdkversions JDK 8, 17, 19 etc.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants