apache/dubbo · error · IOException
Stream closed
Error message
Stream closed
What it means
Thrown by UnsafeStringReader.ensureOpen() (a private guard invoked by read, read(char[],int,int), skip, ready, reset, and mark) when mString == null, i.e. after close() has been called. close() sets mString = null as its sole action, so every subsequent read-family operation trips this IOException. It is the standard 'use after close' signal matching java.io.StringReader.
Source
Thrown at dubbo-common/src/main/java/org/apache/dubbo/common/io/UnsafeStringReader.java:114
ensureOpen();
mMark = mPosition;
}
@Override
public void reset() throws IOException {
ensureOpen();
mPosition = mMark;
}
@Override
public void close() throws IOException {
mString = null;
}
private void ensureOpen() throws IOException {
if (mString == null) {
throw new IOException("Stream closed");
}
}
}
View on GitHub (pinned to 3a3043227f)
Solutions
- Do not use the reader after close() — treat close() as terminal. If using try-with-resources, move all reads inside the try block.
- Ensure single ownership: only the component that opens the reader closes it.
- If a reader field may be reused, null it out after close and create a fresh UnsafeStringReader per use.
- Catch IOException at the call site and treat 'Stream closed' as end-of-lifecycle rather than retrying.
Example fix
// before
try (UnsafeStringReader r = new UnsafeStringReader(s)) {
parse(r);
}
int c = r.read(); // closed -> IOException
// after
try (UnsafeStringReader r = new UnsafeStringReader(s)) {
parse(r);
int c = r.read(); // read inside the scope
} Defensive patterns
Strategy: try-catch
Validate before calling
// Check whether the reader is still open by attempting ready() inside try-with-resources scope. // There is no public isOpen(); the only signal is the IOException itself.
Try / catch
try {
int c = reader.read();
} catch (IOException e) {
if ("Stream closed".equals(e.getMessage())) {
// reader already closed; treat as end-of-lifecycle, do not reuse
return;
}
throw e;
} Prevention
- Use try-with-resources and keep all reads inside the try block.
- Make a single component own the reader's lifecycle (the opener closes it).
- After close(), null out the reader field so accidental reuse fails fast at a null check.
- Treat 'Stream closed' as a terminal condition — never retry the read on the same instance.
When it happens
Trigger: Any sequence: reader.close(); then reader.read(), reader.skip(n), reader.ready(), reader.reset(), or reader.mark(n) on the same UnsafeStringReader. Also reached if close() is called twice and the second close somehow routes through ensureOpen (it does not — close() itself never calls ensureOpen, so only post-close read operations trigger it).
Common situations: Try-with-resources block where code uses the reader after the try exits; sharing a reader across components where one calls close() and another reads; reusing a cached reader field that was closed by a previous request; finalizer/cleanup ordering bugs.
Related errors
- Read-ahead limit < 0
- Mark buffer is full!
- should mark before reset!
- Negative initial size: ${size}
- Negative buffer size
AI-assisted analysis of apache/dubbo@3a3043227f (2026-08-14).
Data as JSON: /api/errors/89d03ba1629cec7f.
Report an issue: GitHub.