Describe the bug
InputStreamContent built from a mark/reset-capable InputStream loses replayability after the first failed HTTP attempt, causing all retries to fail with "stream closed" exception.
The bug is in RestProxyUtils: when preparing the HTTP request body, it extracts the raw stream from the caller's InputStreamContent, wraps it in a LengthValidatingInputStream, and builds a new InputStreamContent from that wrapper (lines 65–67 and 142–147). The new InputStreamContent correctly detects the stream as replayable (because markSupported() returns true via delegation), stores () -> resettableContent(LengthValidatingInputStream) as its replay lambda, and calls mark() on the LengthValidatingInputStream.
After each failed attempt the Reactor pipeline (via MonoUsing) closes the stream it obtained from toStream(). LengthValidatingInputStream.close() delegates unconditionally to inner.close(), which closes the caller's original stream. On the next retry, resettableContent() calls LengthValidatingInputStream.reset() → inner.reset() → the original stream throws IOException: Stream closed.
Verified present in azure-core 1.55.4 and 1.58.0.
Exception or Stack Trace
java.io.UncheckedIOException: java.io.IOException: Stream closed
at com.azure.core.implementation.util.InputStreamContent.resettableContent(InputStreamContent.java:198)
at com.azure.core.implementation.util.InputStreamContent.lambda$new$0(InputStreamContent.java:65)
at com.azure.core.implementation.util.InputStreamContent.toStream(InputStreamContent.java:106)
at reactor.core.publisher.MonoUsing.subscribe(MonoUsing.java:75)
...
at reactor.netty.channel.ChannelOperationsHandler.channelActive(ChannelOperationsHandler.java:62)
Caused by: java.io.IOException: Stream closed
at MyStream.reset(NobeBufInputStream.java:85)
at com.azure.core.implementation.http.rest.LengthValidatingInputStream.reset(LengthValidatingInputStream.java:95)
at com.azure.core.implementation.util.InputStreamContent.resettableContent(InputStreamContent.java:195)
...
Suppressed: java.util.concurrent.TimeoutException: Channel response timed out after 10000 milliseconds.
Expected behavior
When BinaryData.fromStream(is, length) is called with a mark/reset-capable InputStream and retries are enabled, each retry attempt should be able to reset and re-read the stream. LengthValidatingInputStream should not close the underlying stream (or RestProxyUtils should shield the caller's stream from closure between retry attempts), since the stream is needed for subsequent retries.
A minimal fix would be for RestProxyUtils to wrap the caller's stream in a non-closing decorator before handing it to LengthValidatingInputStream.
Alternatively, LengthValidatingInputStream could avoid closing the inner stream when it is used in a retry context.
Workaround: wrap the stream in a non-closing decorator before passing to BinaryData.fromStream().
Reproducer
Test demonstrating both the bug and workaround is in attached file. AI-created, human-checked.
AzureSdkInputStreamRetryBugTest.java
Describe the bug
InputStreamContentbuilt from a mark/reset-capableInputStreamloses replayability after the first failed HTTP attempt, causing all retries to fail with "stream closed" exception.The bug is in
RestProxyUtils: when preparing the HTTP request body, it extracts the raw stream from the caller'sInputStreamContent, wraps it in aLengthValidatingInputStream, and builds a newInputStreamContentfrom that wrapper (lines 65–67 and 142–147). The newInputStreamContentcorrectly detects the stream as replayable (becausemarkSupported()returns true via delegation), stores() -> resettableContent(LengthValidatingInputStream)as its replay lambda, and callsmark()on theLengthValidatingInputStream.After each failed attempt the Reactor pipeline (via
MonoUsing) closes the stream it obtained fromtoStream().LengthValidatingInputStream.close()delegates unconditionally toinner.close(), which closes the caller's original stream. On the next retry,resettableContent()callsLengthValidatingInputStream.reset()→inner.reset()→ the original stream throwsIOException: Stream closed.Verified present in
azure-core1.55.4 and 1.58.0.Exception or Stack Trace
Expected behavior
When
BinaryData.fromStream(is, length)is called with a mark/reset-capableInputStreamand retries are enabled, each retry attempt should be able to reset and re-read the stream.LengthValidatingInputStreamshould not close the underlying stream (orRestProxyUtilsshould shield the caller's stream from closure between retry attempts), since the stream is needed for subsequent retries.A minimal fix would be for
RestProxyUtilsto wrap the caller's stream in a non-closing decorator before handing it toLengthValidatingInputStream.Alternatively,
LengthValidatingInputStreamcould avoid closing the inner stream when it is used in a retry context.Workaround: wrap the stream in a non-closing decorator before passing to
BinaryData.fromStream().Reproducer
Test demonstrating both the bug and workaround is in attached file. AI-created, human-checked.
AzureSdkInputStreamRetryBugTest.java