Describe the bug
PR #7170 ("Don't return 0 from available()", merged
2026-07-23, released in 2.49.3) changed ResponseInputStream.available() so that an open stream never
returns 0:
@Override
public int available() throws IOException {
if (isClosed()) {
return 0;
}
int estimate = super.available();
// Some utilities like java.util.zip.GZIPInputStream use this incorrectly to determine if the stream is *closed* when the
// return value is 0. Guard against misuse of this by returning 1 instead of 0.
return estimate == 0 ? 1 : estimate;
}
This breaks incremental (streaming) reads for any consumer that wraps a ResponseInputStream in an
InputStreamReader / BufferedReader. Data that was previously handed to the caller as soon as it
arrived is now withheld until the next chunk arrives from the network.
The cause is that the JDK's sun.nio.cs.StreamDecoder uses available() > 0 to decide whether more
input can be obtained without blocking:
private boolean inReady() {
try {
return (((in != null) && (in.available() > 0))
|| (ch instanceof FileChannel)); // ## RBC.available()?
} catch (IOException x) {
return false;
}
}
and in implRead():
if (cr.isUnderflow()) {
if (eof)
break;
if (!cb.hasRemaining())
break;
if ((cb.position() > 0) && !inReady())
break; // Block at most once
int n = readBytes();
...
}
The // Block at most once guard is what makes incremental decoding work:
- Up to 2.49.2: between chunks
available() returned 0, so inReady() was false, the decoder
broke out of the loop and returned the characters it had already decoded. readLine() returned
immediately.
- From 2.49.3:
available() always returns >= 1, so inReady() is always true, the decoder does
not break and instead calls readBytes(), which blocks until the next chunk arrives.
The net effect for a long-lived streaming response (for example Server-Sent Events) is that each message
is delayed by one inter-message interval, and the final message flushes the whole accumulated backlog at
once.
Note also that available() returning 0 for an open stream is explicitly permitted by the
InputStream contract, which specifies it returns "an estimate of the number of bytes that can be read
(or skipped over) from this input stream without blocking." Returning a fabricated 1 to compensate for
a downstream consumer's incorrect assumption changes the meaning of the value for all consumers,
including those that use it correctly.
Regression Issue
Expected Behavior
Reading a streaming response through
new BufferedReader(new InputStreamReader(responseInputStream, StandardCharsets.UTF_8)) should surface
each complete line as soon as that line has been received over the wire, as it did in 2.49.2 and
earlier.
For the reproduction below, which emits three messages 300 ms apart, the expected delivery timestamps are
approximately 0 ms, 300 ms, and 600 ms:
t= 11ms event: E1
t= 311ms event: E2
t= 611ms event: E3
Current Behavior
Each line is withheld until the following chunk arrives, so all three messages are delivered together at
the end of the stream:
t= 901ms event: E1
t= 902ms event: E2
t= 902ms event: E3
There are no errors, exceptions, stack traces, or timeouts — the stream completes successfully and
returns correct, complete data. Total transfer time is also unchanged: the last message arrives at
~900 ms in both the working and broken cases. Only the distribution of delivery times changes.
This makes the regression difficult to detect:
- No exception is thrown and no request fails, so error-rate and fault metrics are unaffected.
- End-to-end duration metrics (e.g. time-to-last-byte) are unaffected.
- A mean "gap between consecutive messages" metric is also unaffected, because both the sum of the gaps
and the number of messages are unchanged — only the per-message distribution shifts (median toward
zero, maximum upward).
The only signal is per-message arrival timing. Because nothing surfaces in logs, there are no relevant
SDK logs or wire logs to attach; the stream content itself is correct and unchanged.
Reproduction Steps
Self-contained SSCCE with no AWS dependencies and no credentials required. It uses two InputStream
implementations that differ only in their available() semantics — one matching behavior up to
2.49.2, one matching 2.49.3+ — each fed three SSE-style messages 300 ms apart and read through
BufferedReader.readLine().
Save as AvailableRepro.java, then javac AvailableRepro.java && java AvailableRepro:
import java.io.*;
import java.util.concurrent.*;
public class AvailableRepro {
/** Simulates a network stream: bytes arrive in chunks, with a delay between chunks. */
static class SlowStream extends InputStream {
private final BlockingQueue<Integer> q = new LinkedBlockingQueue<>();
private volatile boolean done = false;
void feed(String s) { for (byte b : s.getBytes()) q.add((int) (b & 0xff)); }
void finish() { done = true; }
@Override public int read() throws IOException {
try {
Integer b;
while ((b = q.poll(50, TimeUnit.MILLISECONDS)) == null) {
if (done) return -1;
}
return b;
} catch (InterruptedException e) { throw new IOException(e); }
}
@Override public int read(byte[] b, int off, int len) throws IOException {
int first = read();
if (first < 0) return -1;
b[off] = (byte) first;
int n = 1;
while (n < len && !q.isEmpty()) { b[off + n] = (byte) (int) q.poll(); n++; }
return n;
}
// Behavior up to and including 2.49.2: honest estimate, may be 0.
@Override public int available() { return q.size(); }
}
/** Behavior from 2.49.3 onward: never returns 0 while the stream is open. */
static class NeverZeroStream extends SlowStream {
@Override public int available() {
int e = super.available();
return e == 0 ? 1 : e;
}
}
static void run(String label, SlowStream s) throws Exception {
BufferedReader r = new BufferedReader(new InputStreamReader(s, "UTF-8"));
long t0 = System.currentTimeMillis();
Thread producer = new Thread(() -> {
try {
for (int i = 1; i <= 3; i++) {
s.feed("event: E" + i + "\n\n");
Thread.sleep(300);
}
s.finish();
} catch (Exception ignored) { }
});
producer.start();
System.out.println("== " + label);
String line;
while ((line = r.readLine()) != null) {
if (!line.isEmpty()) {
System.out.printf(" t=%4dms %s%n", System.currentTimeMillis() - t0, line);
}
}
producer.join();
}
public static void main(String[] args) throws Exception {
run("available() may return 0 (<= 2.49.2)", new SlowStream());
run("available() never returns 0 (>= 2.49.3)", new NeverZeroStream());
}
}
Actual output:
== available() may return 0 (<= 2.49.2)
t= 11ms event: E1
t= 311ms event: E2
t= 611ms event: E3
== available() never returns 0 (>= 2.49.3)
t= 901ms event: E1
t= 902ms event: E2
t= 902ms event: E3
The same behavior can be observed against a real client by consuming any streaming operation that
returns ResponseInputStream through an InputStreamReader, logging a timestamp per line, and comparing
2.49.2 against 2.49.3. The snippet above isolates the mechanism so it can be run without AWS setup.
Possible Solution
A few options, roughly in order of preference:
-
Scope the workaround to the case that motivated it. The linked JDK issues
(JDK-7036144,
JDK-8374644) concern GZIPInputStream misusing
available(). If the SDK wraps or passes streams to GZIP decoding internally, the compensation could
be applied at that boundary rather than on the generally-exposed ResponseInputStream.
-
Make it opt-in. Preserve standard InputStream semantics by default, and allow the never-zero
behavior to be enabled explicitly by callers who need to feed the stream to GZIPInputStream.
-
If the behavior must remain the default, please document it prominently — in the available()
Javadoc, the changelog entry, and ideally the upgrade notes — as affecting the timing of incremental
InputStreamReader / BufferedReader reads, with guidance for streaming consumers. The current
changelog entry accurately describes what changed but does not mention the effect on incremental
read timing, so affected users have no signal connecting a latency-profile change to this release.
The underlying GZIPInputStream behavior is a known JDK defect. Compensating for it inside a widely used,
publicly exposed stream type moves the cost onto every other consumer of that type, including those
relying on the documented available() contract.
Additional Information/Context
Discovered when a service that consumes a streaming (SSE) response incrementally lost progressive
delivery after a dependency upgrade that included 2.49.3. Because there were no errors and total request
duration was unchanged, the only visible symptom was that messages which had previously arrived
progressively began arriving together at the end of the response.
Reading the response body via BufferedReader.readLine() appears to be a common way to consume
line-delimited streaming responses such as SSE, so the impact is likely broader than a single caller.
The failure mode is silent by construction: no exception, no fault, no change in total duration, and no
change in the data returned.
Workarounds identified:
- Pin to a version prior to 2.49.3 (2.49.2 is the last known good), or
- Avoid
InputStreamReader / BufferedReader for the response body and frame lines directly from the
raw bytes, splitting on 0x0A before decoding. This is safe for UTF-8, since continuation bytes are
all >= 0x80, so \n cannot appear inside a multi-byte character.
The second workaround means giving up the JDK's tested line-framing and character decoding, which is a
non-trivial amount of hand-rolled I/O to adopt in order to work around a dependency change.
For reference, the review discussion on #7170 covered returning 0 when the stream is closed and
clearing that state on reset(), both of which were addressed. The effect on incremental
InputStreamReader consumers does not appear to have been considered, which is why this is being raised
as a separate issue (the PR is closed and locked, per the automated guidance to open a new issue
referencing it).
AWS Java SDK version used
2.49.3 (regression introduced here; also present in later 2.49.x releases). Last known good: 2.49.2.
JDK version used
openjdk version "21.0.11" 2026-04-21 LTS OpenJDK Runtime Environment Corretto-21.0.11.10.1 (build 21.0.11+10-LTS) OpenJDK 64-Bit Server VM Corretto-21.0.11.10.1 (build 21.0.11+10-LTS, mixed mode, sharing)
Operating System and version
Amazon Linux 2023 (aarch64). Not OS-specific — the mechanism is in the JDK character-decoding layer.
Describe the bug
PR #7170 ("Don't return 0 from
available()", merged2026-07-23, released in 2.49.3) changed
ResponseInputStream.available()so that an open stream neverreturns
0:This breaks incremental (streaming) reads for any consumer that wraps a
ResponseInputStreamin anInputStreamReader/BufferedReader. Data that was previously handed to the caller as soon as itarrived is now withheld until the next chunk arrives from the network.
The cause is that the JDK's
sun.nio.cs.StreamDecoderusesavailable() > 0to decide whether moreinput can be obtained without blocking:
and in
implRead():The
// Block at most onceguard is what makes incremental decoding work:available()returned0, soinReady()wasfalse, the decoderbroke out of the loop and returned the characters it had already decoded.
readLine()returnedimmediately.
available()always returns>= 1, soinReady()is alwaystrue, the decoder doesnot break and instead calls
readBytes(), which blocks until the next chunk arrives.The net effect for a long-lived streaming response (for example Server-Sent Events) is that each message
is delayed by one inter-message interval, and the final message flushes the whole accumulated backlog at
once.
Note also that
available()returning0for an open stream is explicitly permitted by theInputStreamcontract, which specifies it returns "an estimate of the number of bytes that can be read(or skipped over) from this input stream without blocking." Returning a fabricated
1to compensate fora downstream consumer's incorrect assumption changes the meaning of the value for all consumers,
including those that use it correctly.
Regression Issue
Expected Behavior
Reading a streaming response through
new BufferedReader(new InputStreamReader(responseInputStream, StandardCharsets.UTF_8))should surfaceeach complete line as soon as that line has been received over the wire, as it did in 2.49.2 and
earlier.
For the reproduction below, which emits three messages 300 ms apart, the expected delivery timestamps are
approximately 0 ms, 300 ms, and 600 ms:
Current Behavior
Each line is withheld until the following chunk arrives, so all three messages are delivered together at
the end of the stream:
There are no errors, exceptions, stack traces, or timeouts — the stream completes successfully and
returns correct, complete data. Total transfer time is also unchanged: the last message arrives at
~900 ms in both the working and broken cases. Only the distribution of delivery times changes.
This makes the regression difficult to detect:
and the number of messages are unchanged — only the per-message distribution shifts (median toward
zero, maximum upward).
The only signal is per-message arrival timing. Because nothing surfaces in logs, there are no relevant
SDK logs or wire logs to attach; the stream content itself is correct and unchanged.
Reproduction Steps
Self-contained SSCCE with no AWS dependencies and no credentials required. It uses two
InputStreamimplementations that differ only in their
available()semantics — one matching behavior up to2.49.2, one matching 2.49.3+ — each fed three SSE-style messages 300 ms apart and read through
BufferedReader.readLine().Save as
AvailableRepro.java, thenjavac AvailableRepro.java && java AvailableRepro:Actual output:
The same behavior can be observed against a real client by consuming any streaming operation that
returns
ResponseInputStreamthrough anInputStreamReader, logging a timestamp per line, and comparing2.49.2 against 2.49.3. The snippet above isolates the mechanism so it can be run without AWS setup.
Possible Solution
A few options, roughly in order of preference:
Scope the workaround to the case that motivated it. The linked JDK issues
(JDK-7036144,
JDK-8374644) concern
GZIPInputStreammisusingavailable(). If the SDK wraps or passes streams to GZIP decoding internally, the compensation couldbe applied at that boundary rather than on the generally-exposed
ResponseInputStream.Make it opt-in. Preserve standard
InputStreamsemantics by default, and allow the never-zerobehavior to be enabled explicitly by callers who need to feed the stream to
GZIPInputStream.If the behavior must remain the default, please document it prominently — in the
available()Javadoc, the changelog entry, and ideally the upgrade notes — as affecting the timing of incremental
InputStreamReader/BufferedReaderreads, with guidance for streaming consumers. The currentchangelog entry accurately describes what changed but does not mention the effect on incremental
read timing, so affected users have no signal connecting a latency-profile change to this release.
The underlying
GZIPInputStreambehavior is a known JDK defect. Compensating for it inside a widely used,publicly exposed stream type moves the cost onto every other consumer of that type, including those
relying on the documented
available()contract.Additional Information/Context
Discovered when a service that consumes a streaming (SSE) response incrementally lost progressive
delivery after a dependency upgrade that included 2.49.3. Because there were no errors and total request
duration was unchanged, the only visible symptom was that messages which had previously arrived
progressively began arriving together at the end of the response.
Reading the response body via
BufferedReader.readLine()appears to be a common way to consumeline-delimited streaming responses such as SSE, so the impact is likely broader than a single caller.
The failure mode is silent by construction: no exception, no fault, no change in total duration, and no
change in the data returned.
Workarounds identified:
InputStreamReader/BufferedReaderfor the response body and frame lines directly from theraw bytes, splitting on
0x0Abefore decoding. This is safe for UTF-8, since continuation bytes areall
>= 0x80, so\ncannot appear inside a multi-byte character.The second workaround means giving up the JDK's tested line-framing and character decoding, which is a
non-trivial amount of hand-rolled I/O to adopt in order to work around a dependency change.
For reference, the review discussion on #7170 covered returning
0when the stream is closed andclearing that state on
reset(), both of which were addressed. The effect on incrementalInputStreamReaderconsumers does not appear to have been considered, which is why this is being raisedas a separate issue (the PR is closed and locked, per the automated guidance to open a new issue
referencing it).
AWS Java SDK version used
2.49.3 (regression introduced here; also present in later 2.49.x releases). Last known good: 2.49.2.
JDK version used
openjdk version "21.0.11" 2026-04-21 LTS OpenJDK Runtime Environment Corretto-21.0.11.10.1 (build 21.0.11+10-LTS) OpenJDK 64-Bit Server VM Corretto-21.0.11.10.1 (build 21.0.11+10-LTS, mixed mode, sharing)
Operating System and version
Amazon Linux 2023 (aarch64). Not OS-specific — the mechanism is in the JDK character-decoding layer.