Skip to content

BaseEncoding: Make encodingStream().close() idempotent#8183

Open
nickita-khylkouski wants to merge 1 commit intogoogle:masterfrom
nickita-khylkouski:fix-encoding-stream-close-idempotent
Open

BaseEncoding: Make encodingStream().close() idempotent#8183
nickita-khylkouski wants to merge 1 commit intogoogle:masterfrom
nickita-khylkouski:fix-encoding-stream-close-idempotent

Conversation

@nickita-khylkouski
Copy link
Contributor

Summary

  • Add boolean closed field to the OutputStream returned by encodingStream()
  • Make write(), flush(), and close() synchronized per @cpovirk's guidance
  • close() returns early if already closed (idempotent)
  • write()/flush() throw IOException after close, matching AppendableWriter and CharSequenceReader patterns

Motivation

BaseEncoding.encodingStream().close() violates the Closeable.close() contract which states: "If the stream is already closed then invoking this method has no effect."

Calling close() twice writes additional encoded data to the underlying Writer:

StringWriter w = new StringWriter();
OutputStream out = BaseEncoding.base64().encodingStream(w);
out.write(0);
out.close();
System.out.println(w); // "AA==" (correct)
out.close();
System.out.println(w); // "AA==A===" (wrong - extra data)

This is problematic because double-close is common due to try-with-resources blocks and wrapping streams that close their underlying streams.

Fixes #5284

Testing

  • 3 new tests in BaseEncodingTest:
    • testEncodingStreamCloseIsIdempotent — verifies close() twice doesn't change output
    • testEncodingStreamWriteAfterClose — verifies write() throws IOException after close
    • testEncodingStreamFlushAfterClose — verifies flush() throws IOException after close
  • All 43 tests in BaseEncodingTest pass (./mvnw test -pl guava-tests -Dtest=BaseEncodingTest)
  • Added IOException to ReflectionFreeAssertThrows to support the new test assertions

The OutputStream returned by encodingStream() violated the
Closeable.close() contract: calling close() multiple times would
write additional encoded data to the underlying Writer. This adds
a boolean closed field and synchronized methods to track stream
state, making close() idempotent and causing write()/flush() to
throw IOException after close.

Fixes google#5284
public void write(int b) throws IOException {
public synchronized void write(int b) throws IOException {
if (closed) {
throw new IOException("Stream is closed");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Steam is closed" is repeated twice. Shouldn't that be defined as a reusable constant?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Guava pattern for this is a checkNotClosed() helper method, not a string constant — see AppendableWriter. Since this is a small anonymous class with only two call sites, I kept it inline, but happy to extract a helper if maintainers prefer.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough.


@Override
public void write(int b) throws IOException {
public synchronized void write(int b) throws IOException {
Copy link

@softpapers softpapers Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Synchronization is required for reliable communication between threads as well as for mutual exclusion.

  2. Synchronization is not guaranteed to work unless both read and write operations are synchronized.

"Occasionally a program that synchronizes only writes (or reads) may appear to work on some machines, but in this case, appearances are deceiving."

--- Item 78 of Effective Java Programming by Joshua Blosch.

What justifies the need for the write of an abstract class to be synchronised without synchronising the read?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All three methods that access the closed field — write(), flush(), and close() — are synchronized. Both the write to closed (in close()) and the reads of closed (in write() and flush()) happen within synchronized methods. There is no unsynchronized read.

This matches CharSequenceReader, which synchronizes every method. The synchronization was added per @cpovirk's guidance to ensure close() and write() cannot interleave.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BaseEncoding.encodingStream().close() is non-idempotent

3 participants