Skip to content

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

@OmarAlJarrah

Description

@OmarAlJarrah

Problem

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

private fun closeDelegateOnce() {
    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:

private fun closeDelegateOnce() {
    if (delegateClosed) return
    try {
        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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions