eclipse-vertx/vert.x · error · IllegalStateException
Response has already been written
Error message
Response has already been written
What it means
write_() is the internal path behind response.write() and end(). Once the response has ended (ended == true), any further write or end attempt throws IllegalStateException because the response is committed and the underlying stream is finished — HTTP does not allow writing after the response completed.
Source
Thrown at vertx-core/src/main/java/io/vertx/core/http/impl/HttpServerResponseImpl.java:398
future = stream.writeHeaders(trailedMap, true);
}
Handler<Void> bodyEndHandler = this.bodyEndHandler;
Handler<Void> endHandler = this.endHandler;
if (bodyEndHandler != null) {
bodyEndHandler.handle(null);
}
if (endHandler != null) {
endHandler.handle(null);
}
}
return future;
}
private Future<Void> write_(Buffer chunk, boolean end) {
boolean sendHeaders;
synchronized (conn) {
if (ended) {
throw new IllegalStateException("Response has already been written");
}
ended = end;
if (end && !headWritten && requiresContentLengthHeader()) {
headers().set(HttpHeaderNames.CONTENT_LENGTH, chunk == null ? "0" : HttpUtils.positiveLongToString(chunk.length()));
}
sendHeaders = prepareHeaders();
}
if (sendHeaders) {
return stream.writeHead(new HttpResponseHead(status.code(), status.reasonPhrase(), headersMap), chunk, end);
} else {
return stream.writeChunk(chunk, end);
}
}
private boolean requiresContentLengthHeader() {
return requestMethod != HttpMethod.HEAD && status != HttpResponseStatus.NOT_MODIFIED && !headersMap.contains(HttpHeaderNames.CONTENT_LENGTH);
}
View on GitHub (pinned to fb308bd8c3)
Solutions
- Ensure exactly one write/end path executes — guard with a boolean or use response.ended() if available
- In error handlers, check whether the response is already ended before attempting to write an error response
- Centralize response completion in a single helper that is idempotent
- Structure async flows with flatMap/transform so only one completion path runs
Example fix
// before
response.end("ok");
onError(err -> response.end("error")); // IllegalStateException if success path ran
// after
AtomicBoolean done = new AtomicBoolean();
void respond(String body) {
if (done.compareAndSet(false, true)) response.end(body);
} Defensive patterns
Strategy: try-catch
Validate before calling
if (!response.ended()) {
response.end(body);
} Try / catch
try {
response.end(chunk);
} catch (IllegalStateException e) {
// response already written; skip
} Prevention
- Ensure a single completion path per response
- Check response.ended() in error handlers before writing
- Use idempotent response helpers with a done flag
When it happens
Trigger: Calling write() or end() after end() already succeeded; multiple end() calls; writing from a callback that fires after another code path ended the response.
Common situations: Error-handling code calling end() after a success path already wrote the response; async callbacks racing to write; retry logic re-invoking end() on failure; double dispatch in routing.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Not a streaming upload
- Request has already been read
- Response head already sent
- Response has already been written
- Head already written
AI-assisted analysis of eclipse-vertx/vert.x@fb308bd8c3 (2026-09-06).
Data as JSON: /api/errors/0683f8c9e8077e54.
Report an issue: GitHub.