quarkusio/quarkus · error · IOException

Stream is closed

Error message

Stream is closed

What it means

VertxOutputStream.write(byte[], int, int) throws IOException("Stream is closed") when the response output stream has already been closed and a further write is attempted. After close(), the underlying Vert.x response ends and any additional byte writes are invalid.

Source

Thrown at extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/VertxOutputStream.java:58

        write(new byte[] { (byte) b }, 0, 1);
    }

    /**
     * {@inheritDoc}
     */
    public void write(final byte[] b) throws IOException {
        write(b, 0, b.length);
    }

    /**
     * {@inheritDoc}
     */
    public void write(final byte[] b, final int off, final int len) throws IOException {
        if (len < 1) {
            return;
        }
        if (closed) {
            throw new IOException("Stream is closed");
        }

        int rem = len;
        int idx = off;
        ByteBuf buffer = pooledBuffer;
        try {
            if (buffer == null) {
                pooledBuffer = buffer = allocator.allocateBuffer();
            }
            while (rem > 0) {
                int toWrite = Math.min(rem, buffer.writableBytes());
                buffer.writeBytes(b, idx, toWrite);
                rem -= toWrite;
                idx += toWrite;
                if (!buffer.isWritable()) {
                    ByteBuf tmpBuf = buffer;
                    this.pooledBuffer = buffer = allocator.allocateBuffer();
                    response.writeBlocking(tmpBuf, false);

View on GitHub (pinned to e1c734241f)

Solutions

  1. Guard writes with a closed check or restructure so all writes happen before close(); track stream lifetime in one owner class.
  2. Do not write in finally blocks; only clean up resources there.
  3. Catch IOException and treat it as 'client already got the response / stream done' instead of propagating.
  4. Ensure only one component (the framework or your code, not both) closes the stream.

Example fix

// before
stream.close();
stream.write(finishBytes); // IOException: Stream is closed

// after
stream.write(finishBytes);
stream.close();
Defensive patterns

Strategy: validation

Validate before calling

private void safeWrite(OutputStream out, byte[] data) throws IOException {
    if (out == null || !(out instanceof io.quarkus.resteasy.runtime.standalone.VertxOutputStream v) || vIsClosed(v)) {
        return;
    }
    out.write(data);
}

Type guard

boolean isStreamOpen(java.io.OutputStream out) {
    try {
        out.flush();
        return true;
    } catch (IOException e) {
        return !"Stream is closed".equals(e.getMessage());
    }
}

Try / catch

try {
    out.write(chunk);
} catch (IOException e) {
    if ("Stream is closed".equals(e.getMessage())) {
        LOG.debug("Stream already closed; dropping trailing write");
        return;
    }
    throw e;
}

Prevention

When it happens

Trigger: Writing to the response OutputStream after close() was called — e.g. StreamingOutput/StreamingResponse continuing to write after an early return closed the stream, a finally block writing a terminator after close, or writing after the request lifecycle ended the response.

Common situations: Manual streaming code that ignores close semantics; double flush/close in a finally; frameworks (e.g. SSE or file download handlers) closing the stream while an application filter still writes; exception path that closes the stream then logs the error to the response body.

Related errors


AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05). Data as JSON: /api/errors/df99f343197464ed. Report an issue: GitHub.