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

  1. Call sendFile before any other write/end on the response
  2. Remove preceding writeHead()/write() calls when sendFile will be used
  3. 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

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


AI-assisted analysis of eclipse-vertx/vert.x@fb308bd8c3 (2026-09-06). Data as JSON: /api/errors/3813c92df209b237. Report an issue: GitHub.