eclipse-vertx/vert.x · error · IllegalStateException
Head already written
Error message
Head already written
What it means
Thrown by Http1ServerResponse.sendFileInternal (reached via sendFile) when the response head has already been written. sendFile must write status code and headers itself (including Content-Length), so it cannot run once the head is out.
Source
Thrown at vertx-core/src/main/java/io/vertx/core/http/impl/http1/Http1ServerResponse.java:510
private Future<Void> sendFileInternal(long offset, long length, long size, RandomAccessFile file, FileChannel fileChannel, boolean close) {
Future<Void> ret = null;
try {
ContextInternal ctx = vertx.getOrCreateContext();
if (offset < 0) {
return ctx.failedFuture("offset : " + offset + " (expected: >= 0)");
}
if (length < 0) {
return ctx.failedFuture("length : " + length + " (expected: >= 0)");
}
long actualLength = Math.min(length, size - offset);
long actualOffset = Math.min(offset, size);
if (actualLength < 0) {
return ctx.failedFuture("offset : " + offset + " is larger than the requested file length : " + size);
}
synchronized (conn) {
checkValid();
if (headWritten) {
throw new IllegalStateException("Head already written");
}
// fail early before status code/headers are written to the response
prepareHeaders(actualLength);
bytesWritten = actualLength;
written = true;
conn.write(new VertxAssembledHttpResponse(head, version, status, headers), null);
FileChannel toSend = fileChannel == null ? file.getChannel() : fileChannel;
ChannelFuture channelFuture = conn.sendFile(toSend, actualOffset, actualLength);
PromiseInternal<Void> promise = context.promise();
ret = promise.future();
channelFuture.addListener(future -> {
if (future.isSuccess()) {
// signal body end handler
Handler<Void> handler;
synchronized (conn) {
handler = bodyEndHandler;View on GitHub (pinned to fb308bd8c3)
Solutions
- Call sendFile before any other write/end on the response
- Remove preceding writeHead()/write() calls when sendFile will be used
- Return early from the handler once sendFile is invoked
Example fix
// before
response.putHeader("Content-Type", "text/plain");
response.write("hi");
response.sendFile("file.txt");
// after
response.putHeader("Content-Type", "text/plain");
response.sendFile("file.txt"); Defensive patterns
Strategy: validation
Validate before calling
if (!response.headWritten()) {
response.sendFile("/path/file");
} Try / catch
try { response.sendFile(path); } catch (IllegalStateException e) { /* head already written; cannot sendFile now */ } Prevention
- Always sendFile before any write/end
- Do not mix sendFile with manual body writes
- Handle sendFile errors without retrying after header flush
When it happens
Trigger: Calling response.sendFile(path) after headers were flushed — e.g. after writeHead(), after a previous partial sendFile attempt, or after end()/write() implicitly wrote the head.
Common situations: Conditional logic where one branch writes the body/headers and another calls sendFile; retrying a failed sendFile after headers were prepared; sendFile called from both middleware and handler.
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
- Response has already been written
- Response head already sent
- Response has already been written
- Cannot write an HTTP/2 frame over an HTTP/1.x connection
- HTTP/1.x connections don't support SETTINGS
AI-assisted analysis of eclipse-vertx/vert.x@fb308bd8c3 (2026-09-06).
Data as JSON: /api/errors/3813c92df209b237.
Report an issue: GitHub.