quarkusio/quarkus · error · IOException

Connection has been closed

Error message

Connection has been closed

What it means

While waiting for the response write queue to drain, awaitWriteable() checks response.closed(); if Vertx reports the response/connection has closed, it throws IOException('Connection has been closed') because the queued data can never be delivered. This prevents blocking indefinitely on a drain event that will never arrive.

Source

Thrown at independent-projects/vertx-utils/src/main/java/io/quarkus/vertx/utils/VertxOutputStream.java:128

            throw new IOException("Failed to write", e);
        } finally {
            lock.unlock();
        }
    }

    private void awaitWriteable() throws IOException {
        // is it running in an event loop?
        if (Context.isOnEventLoopThread()) {
            // NEVER block the event loop!
            return;
        }
        assert lock.isHeldByCurrentThread();
        while (response.writeQueueFull()) {
            if (throwable != null) {
                throw new IOException(throwable);
            }
            if (response.closed()) {
                throw new IOException("Connection has been closed");
            }
            try {
                waitingForDrain = true;
                // To make sure we don't get stuck waiting, we time out periodically
                drainCondition.await(1, TimeUnit.SECONDS);
                // Check for timeout / closed connection
                if (request.response().ended() || request.response().closed()) {
                    if (throwable != null) {
                        // We had an exception, propagate it
                        throw new IOException(throwable);
                    } else {
                        throw new IOException("Connection has been closed");
                    }
                }
            } catch (InterruptedException e) {
                throw new InterruptedIOException(e.getMessage());
            } finally {
                waitingForDrain = false;

View on GitHub (pinned to e1c734241f)

Solutions

  1. Treat this as a normal client-disconnect signal: catch it, release resources, and stop producing data
  2. Tune server idle/timeouts if legitimate slow clients are being cut off
  3. Write smaller chunks more frequently so the queue drains between writes
  4. Add a drain/close handler on the response to abort generation work early

Example fix

// before
while (hasMore()) {
    out.write(nextChunk()); // throws once client disconnects
}
// after
try {
    while (hasMore() && !response.closed()) {
        out.write(nextChunk());
    }
} catch (IOException closed) {
    cancelGeneration();
}
Defensive patterns

Strategy: try-catch

Try / catch

try {
    out.writeBlocking(chunk);
} catch (IOException e) {
    if (e.getCause() != null) {
        logger.warn("Connection failure during stream: {}", e.getCause().getMessage());
    }
    cleanup();
}

Prevention

When it happens

Trigger: write() is called, the write queue is full, and the peer closes the TCP connection (or the response is closed server-side) before the queue drains.

Common situations: User cancels a download; mobile clients switching networks mid-stream; reverse proxy closing upstream connections after idle timeout; server-side timeouts (e.g. quarkus.http.idle-timeout) firing during a slow stream.

Related errors


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