On the over-cap path, LoggableResponseBody.source() hands out a one-shot stream whose tail is the live delegate source:
val tail = liveTail ?: return buf.peek()
return provider.bufferedSource(PrefixThenTailSource(buf.peek(), tail))
where liveTail == delegate.source() (sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/response/LoggableResponseBody.kt:138-148).
Two independent paths can then close that same source:
PrefixThenTailSource.close() closes tail (:304-310)
close() on the wrapper closes delegate (:193-196), which per the transport contract cascades into the same source (OkHttp ResponseAdapter: "closing the SDK response cascades into the okio source, which closes the OkHttp body").
A consumer that does body.source().use { … } and closes the Response closes the underlying transport source twice. It is currently saved only by okio's idempotent close() — the exact assumption the rest of this class refuses to make (the delegateClosed bookkeeping exists precisely because "some sockets / streams throw on double-close"). With the default 8 KiB preview, any response body over 8 KiB under BODY_AND_HEADERS takes this path, and the wrapper is provider-agnostic.
Suggested fix
Track whether the live tail was already closed (or route the tail close through the same delegateClosed guard so the underlying source is closed at most once), and add a test that closes the handed-out source and then the response.
Minor (same method)
The liveTail ?: return buf.peek() fallback at :146 is unreachable after a successful drain — the drain always sets either fullyCaptured or liveTail, otherwise drainError throws first — yet it still consumes the single-use token. A check(liveTail != null) would state the invariant instead of silently falling back.
Code added on fix/http-stack-correctness-resource-safety (#13).
On the over-cap path,
LoggableResponseBody.source()hands out a one-shot stream whose tail is the live delegate source:where
liveTail == delegate.source()(sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/response/LoggableResponseBody.kt:138-148).Two independent paths can then close that same source:
PrefixThenTailSource.close()closestail(:304-310)close()on the wrapper closesdelegate(:193-196), which per the transport contract cascades into the same source (OkHttpResponseAdapter: "closing the SDK response cascades into the okio source, which closes the OkHttp body").A consumer that does
body.source().use { … }and closes theResponsecloses the underlying transport source twice. It is currently saved only by okio's idempotentclose()— the exact assumption the rest of this class refuses to make (thedelegateClosedbookkeeping exists precisely because "some sockets / streams throw on double-close"). With the default 8 KiB preview, any response body over 8 KiB underBODY_AND_HEADERStakes this path, and the wrapper is provider-agnostic.Suggested fix
Track whether the live tail was already closed (or route the tail close through the same
delegateClosedguard so the underlying source is closed at most once), and add a test that closes the handed-out source and then the response.Minor (same method)
The
liveTail ?: return buf.peek()fallback at:146is unreachable after a successful drain — the drain always sets eitherfullyCapturedorliveTail, otherwisedrainErrorthrows first — yet it still consumes the single-use token. Acheck(liveTail != null)would state the invariant instead of silently falling back.Code added on
fix/http-stack-correctness-resource-safety(#13).