Skip to content

LoggableResponseBody: a failed delegate.close() defeats the single-close guard #115

Description

@OmarAlJarrah

Problem

LoggableResponseBody.closeDelegateOnce() (the single-close guard added in #80) sets the
delegateClosed flag only after delegate.close() returns normally:

privatefuncloseDelegateOnce() {
if (!delegateClosed) {
delegate.close()
delegateClosed =true
}
}

If delegate.close() throws, delegateClosed is left false. Both close() and the over-cap
one-shot source (PrefixThenTailSource.close()) reach the delegate through this method from
independent entry points, so a failure on the first path leaves the guard unset and the second
path closes the delegate again.

On the over-cap path:

  1. The consumer closes the one-shot source → closeDelegateOnce()delegate.close() throws →
    guard stays false.
  2. The consumer later closes the wrapper → closeDelegateOnce()delegate.close() runs a
    second time.

ResponseBody.close() is documented as idempotent, so a conforming delegate tolerates the repeat
call. But the guard exists precisely to guarantee a single close for delegates whose handles are
not safe to close twice, and the error path silently breaks that guarantee.

Inconsistency

The drain-path error handler in drainAndCache() does the opposite: when closing the source after
a failed read throws, it still marks the delegate closed so a later close() is a no-op.
closeDelegateOnce() should match that behavior.

Suggested fix

Set the guard whether or not delegate.close() throws:

privatefuncloseDelegateOnce() {
if (delegateClosed) returntry {
delegate.close()
} finally {
delegateClosed =true
}
}

This keeps "closed at most once" true even when the close itself fails, and lets the exception
propagate as before.

Severity

Low — it only triggers when delegate.close() throws, and the public contract already asks
delegates to be idempotent. Worth fixing for consistency with the drain path and to keep the
single-close guarantee honest.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions