Skip to content

[Bug] StreamUtils treats InputStream.available() as the remaining stream length #16510

Open

Description

@hickey-ai

Pre-check

  • I am sure that all the content I provide is in English.

Search before asking

  • I had searched in the issues and found no similar issues.

Apache Dubbo Component

Java SDK (apache/dubbo)

Dubbo Version

Apache Dubbo source: 3.3.7-SNAPSHOT, branch 3.3, commit ae809de2fc81b408194240965eb20bf27e6971df.
Windows 11, OpenJDK 21, Maven 3.6.3.

Two focused utility regressions were run in an isolated Maven/JUnit harness using byte-for-byte source copies. No registry or network service is required.

Steps to reproduce this issue

Component

Java SDK (apache/dubbo), dubbo-common.

Summary

StreamUtils.skipUnusedStream(InputStream) uses InputStream.available() to decide whether to skip data, and performs only one skip call. This is incorrect for general InputStream implementations: available() may legally return zero even when a read would produce data, and skip(n) may skip fewer than n bytes. The method can leave request/serialization bytes unread.

The same available() assumption also appears in limitedInputStream, which sets its limit to Math.min(limit, is.available()); a stream with data but available() == 0 is exposed as immediately exhausted.

Version and verification scope

  • Apache Dubbo 3.3.7-SNAPSHOT, branch 3.3, commit ae809de2fc81b408194240965eb20bf27e6971df.
  • Windows 11, OpenJDK 21, Maven 3.6.3.
  • Two implementation files were copied byte-for-byte from the commit into an isolated Maven/JUnit harness. No network service or registry is required.
  • This is a focused utility regression, not a full reactor build or production protocol validation.

Reproduction

static class AvailableZeroStream extends InputStream {
    private final byte[] data = "payload".getBytes();
    private int index;
    public int available() { return 0; }
    public int read() { return index < data.length ? data[index++] & 0xff : -1; }
    public long skip(long n) {
        long amount = Math.min(n, Math.max(0, data.length - index));
        index += (int) amount;
        return amount;
    }
}

@Test
void skipUnusedStreamMustDrainStreamsWhoseAvailableIsZero() throws Exception {
    AvailableZeroStream in = new AvailableZeroStream();
    StreamUtils.skipUnusedStream(in);
    assertEquals(-1, in.read()); // fails: returns 'p'
}

@Test
void skipUnusedStreamMustContinueWhenSkipReturnsLessThanAvailable() throws Exception {
    InputStream in = new InputStream() {
        private int remaining = 5;
        public int available() { return remaining; }
        public int read() { if (remaining == 0) return -1; remaining--; return 'x'; }
        public long skip(long n) { long skipped = Math.min(1, n); remaining -= skipped; return skipped; }
    };
    StreamUtils.skipUnusedStream(in);
    assertEquals(-1, in.read()); // fails: returns 'x'
}

Executed result: both assertions failed; the normal JUnit control for Ring passed. The failure is caused by the actual upstream StreamUtils class.

Expected behavior

A utility that drains or limits an input stream should use read()/skip() progress until EOF or the requested limit is reached. available() is only an estimate of non-blocking bytes and must not be used as the stream's total length. If the intended contract is only “skip currently available bytes”, the method name and documentation should state that explicitly and the one-shot skip should still handle partial progress.

Root cause

skipUnusedStream guards the only skip call with is.available() > 0. limitedInputStream similarly uses is.available() as its initial limit. Both paths break for standard custom/network/decompressing streams where available() is zero or smaller than the remaining data.

Deduplication

Searched open issues for StreamUtils, available skip, skipUnusedStream, and stream payload handling. I did not find a report of this root cause in the searched results. This is separate from the existing Bytes/InputStream MD5 report #16451.

What you expected to happen

Stream utility methods should not use InputStream.available() as the total remaining length. A valid stream with available()==0 must not be treated as empty, and a partial skip must be retried until the intended data is consumed.

Both focused regressions failed against the unchanged upstream implementation.

Anything else

Root cause: skipUnusedStream checks available()>0 and performs one skip call; limitedInputStream also derives its limit from available(). InputStream.available() is only a non-blocking estimate and skip() may return fewer bytes than requested.

Searched open issues for StreamUtils, available skip, skipUnusedStream and stream payload handling; no matching root-cause report was found. This is separate from the existing Bytes/InputStream MD5 report #16451.

Do you have a (mini) reproduction demo?

  • Yes, I have a minimal reproduction demo to help resolve this issue more effectively!

Are you willing to submit a pull request to fix on your own?

  • Yes I am willing to submit a pull request on my own!

Code of Conduct

Activity

  1. Yeti11H commented on Oct 17, 2026

    @Yeti11H

    Hi,

    I would like to investigate this issue.

    I can reproduce the behavior with custom InputStream implementations where
    available() returns zero or skip() makes only partial progress. I plan to
    first confirm the intended contract of skipUnusedStream() and
    limitedInputStream(), then prepare a minimal fix with focused regression
    tests in dubbo-common.

    I have not started code changes yet. Please let me know if anyone is
    already working on this issue or if there are compatibility considerations
    for the current stream behavior.

  2. hickey-ai commented on Oct 17, 2026

    @hickey-ai
    ContributorAuthor

    @Yeti11H A proposed fix is already open in #16511, so please check that PR before starting a separate implementation. It includes regressions for short reads/skips and a stream with no immediately available bytes. One compatibility point is important: cleanup remains bounded to the initially available bytes; it does not drain an unbounded stream to EOF or wait for future data. The limited stream uses the caller's requested limit and accounts for actual read/skip progress. These are the semantics proposed in the PR, still subject to maintainer review. Your independent review of that boundary would be useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions