Repository: eclipse-vertx/vert.x

Problem statement:
Extra chunk for request with no content.
The following code produces an extra 0 end trailer chunk that makes HTTP clients parse it as a second invalid response:

```java
		Vertx vertx = Vertx.vertx();
		HttpServer server = vertx.createHttpServer();
		server.requestStream().toObservable().subscribe(req -> {
			System.err.println("Got request");
			req.response().setChunked(true).setStatusCode(204).end();
		});
		server.rxListen(9001, "localhost").subscribe(s -> System.err.println("started"), Throwable::printStackTrace);
```

My guess is that if there is no body, there should not be any trailer end chunk.

Related to #1986 (was only for HEAD).

https://tools.ietf.org/html/rfc7231#section-6.3.5 indicates:

     A 204 response is terminated by the first empty line after the header
     fields because it cannot contain a message body.

Perhaps there are other HTTP status codes that can't contain a message body?


Hints:
taht should be easy to fix
1xx code should not contain response body
I found the following conditions where the spec forbids a body:

```
(status >= 100 && status < 200)
				|| status == Status.NO_CONTENT.getStatusCode()
				|| status == Status.NOT_MODIFIED.getStatusCode()
```
currently I do have:
```
    boolean isChunked = chunked && !head
      && status != HttpResponseStatus.NO_CONTENT
      && status.code() / 100 > 1;
```

Patch:
diff --git a/src/main/java/io/vertx/core/http/impl/Http2ServerConnection.java b/src/main/java/io/vertx/core/http/impl/Http2ServerConnection.java
--- a/src/main/java/io/vertx/core/http/impl/Http2ServerConnection.java
+++ b/src/main/java/io/vertx/core/http/impl/Http2ServerConnection.java
@@ -34,6 +34,8 @@
 import java.net.URISyntaxException;
 import java.util.ArrayDeque;
 
+import static io.vertx.core.spi.metrics.Metrics.METRICS_ENABLED;
+
 /**
  * @author <a href="mailto:julien@julienviet.com">Julien Viet</a>
  */
@@ -278,7 +280,10 @@ void handleClose() {
 
     void complete() {
       synchronized (Http2ServerConnection.this) {
-        response = new Http2ServerResponseImpl(Http2ServerConnection.this, this, method, uri, true, contentEncoding);
+        response = new Http2ServerResponseImpl(Http2ServerConnection.this, this, method, true, contentEncoding, null);
+        if (METRICS_ENABLED && metrics != null) {
+          response.metric(metrics.responsePushed(conn.metric(), method, uri, response));
+        }
         completionHandler.complete(response);
       }
     }
diff --git a/src/main/java/io/vertx/core/http/impl/Http2ServerRequestImpl.java b/src/main/java/io/vertx/core/http/impl/Http2ServerRequestImpl.java
--- a/src/main/java/io/vertx/core/http/impl/Http2ServerRequestImpl.java
+++ b/src/main/java/io/vertx/core/http/impl/Http2ServerRequestImpl.java
@@ -92,8 +92,10 @@ public Http2ServerRequestImpl(Http2ServerConnection conn, Http2Stream stream, Ht
       int idx = serverOrigin.indexOf("://");
       host = serverOrigin.substring(idx + 3);
     }
-    Object metric = (METRICS_ENABLED && metrics != null) ? metrics.requestBegin(conn.metric(), this) : null;
-    this.response = new Http2ServerResponseImpl(conn, this, metric, false, contentEncoding, host);
+    this.response = new Http2ServerResponseImpl(conn, this, method(), false, contentEncoding, host);
+    if (METRICS_ENABLED && metrics != null) {
+      response.metric(metrics.requestBegin(conn.metric(), this));
+    }
   }
 
   @Override
diff --git a/src/main/java/io/vertx/core/http/impl/Http2ServerResponseImpl.java b/src/main/java/io/vertx/core/http/impl/Http2ServerResponseImpl.java
--- a/src/main/java/io/vertx/core/http/impl/Http2ServerResponseImpl.java
+++ b/src/main/java/io/vertx/core/http/impl/Http2ServerResponseImpl.java
@@ -17,6 +17,7 @@
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.handler.codec.http.HttpHeaderNames;
 import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpStatusClass;
 import io.netty.handler.codec.http2.DefaultHttp2Headers;
 import io.netty.handler.codec.http2.Http2Headers;
 import io.vertx.codegen.annotations.Nullable;
@@ -26,6 +27,7 @@
 import io.vertx.core.Handler;
 import io.vertx.core.MultiMap;
 import io.vertx.core.buffer.Buffer;
+import io.vertx.core.http.HttpHeaders;
 import io.vertx.core.http.HttpMethod;
 import io.vertx.core.http.HttpServerResponse;
 import io.vertx.core.http.StreamResetException;
@@ -53,17 +55,18 @@ public class Http2ServerResponseImpl implements HttpServerResponse {
   private final VertxHttp2Stream stream;
   private final ChannelHandlerContext ctx;
   private final Http2ServerConnection conn;
+  private final boolean head;
   private final boolean push;
-  private final Object metric;
   private final String host;
   private Http2Headers headers = new DefaultHttp2Headers();
+  private Object metric;
   private Http2HeadersAdaptor headersMap;
   private Http2Headers trailers;
   private Http2HeadersAdaptor trailedMap;
   private boolean chunked;
   private boolean headWritten;
   private boolean ended;
-  private int statusCode = 200;
+  private HttpResponseStatus status = HttpResponseStatus.OK;
   private String statusMessage; // Not really used but we keep the message for the getStatusMessage()
   private Handler<Void> drainHandler;
   private Handler<Throwable> exceptionHandler;
@@ -76,12 +79,16 @@ public class Http2ServerResponseImpl implements HttpServerResponse {
   private boolean inHandler;
   private NetSocket netSocket;
 
-  public Http2ServerResponseImpl(Http2ServerConnection conn, VertxHttp2Stream stream, Object metric, boolean push, String contentEncoding, String host) {
-
-    this.metric = metric;
+  public Http2ServerResponseImpl(Http2ServerConnection conn,
+                                 VertxHttp2Stream stream,
+                                 HttpMethod method,
+                                 boolean push,
+                                 String contentEncoding,
+                                 String host) {
     this.stream = stream;
     this.ctx = conn.handlerContext;
     this.conn = conn;
+    this.head = method == HttpMethod.HEAD;
     this.push = push;
     this.host = host;
 
@@ -90,25 +97,8 @@ public Http2ServerResponseImpl(Http2ServerConnection conn, VertxHttp2Stream stre
     }
   }
 
-  public Http2ServerResponseImpl(
-      Http2ServerConnection conn,
-      VertxHttp2Stream stream,
-      HttpMethod method,
-      String path,
-      boolean push,
-      String contentEncoding) {
-    this.stream = stream;
-    this.ctx = conn.handlerContext;
-    this.conn = conn;
-    this.push = push;
-    this.host = null;
-
-    if (contentEncoding != null) {
-      putHeader(HttpHeaderNames.CONTENT_ENCODING, contentEncoding);
-    }
-
-    HttpServerMetrics metrics = conn.metrics();
-    this.metric = (METRICS_ENABLED && metrics != null) ? metrics.responsePushed(conn.metric(), method, path, this) : null;
+  void metric(Object metric) {
+    this.metric = metric;
   }
 
   synchronized void beginRequest() {
@@ -155,7 +145,7 @@ public HttpServerResponse exceptionHandler(Handler<Throwable> handler) {
   @Override
   public int getStatusCode() {
     synchronized (conn) {
-      return statusCode;
+      return status.code();
     }
   }
 
@@ -166,7 +156,7 @@ public HttpServerResponse setStatusCode(int statusCode) {
     }
     synchronized (conn) {
       checkHeadWritten();
-      this.statusCode = statusCode;
+      this.status = HttpResponseStatus.valueOf(statusCode);
       return this;
     }
   }
@@ -175,7 +165,7 @@ public HttpServerResponse setStatusCode(int statusCode) {
   public String getStatusMessage() {
     synchronized (conn) {
       if (statusMessage == null) {
-        return HttpResponseStatus.valueOf(statusCode).reasonPhrase();
+        return status.reasonPhrase();
       }
       return statusMessage;
     }
@@ -374,7 +364,7 @@ public void end() {
   NetSocket netSocket() {
     checkEnded();
     if (netSocket == null) {
-      statusCode = 200;
+      status = HttpResponseStatus.OK;
       if (!checkSendHeaders(false)) {
         throw new IllegalStateException("Response for CONNECT already sent");
       }
@@ -391,16 +381,29 @@ private void end(ByteBuf chunk) {
     }
   }
 
+  private void sanitizeHeaders() {
+    if (head || status == HttpResponseStatus.NOT_MODIFIED) {
+      headers.remove(HttpHeaders.TRANSFER_ENCODING);
+    } else if (status == HttpResponseStatus.RESET_CONTENT) {
+      headers.remove(HttpHeaders.TRANSFER_ENCODING);
+      headers.set(HttpHeaders.CONTENT_LENGTH, "0");
+    } else if (status.codeClass() == HttpStatusClass.INFORMATIONAL || status == HttpResponseStatus.NO_CONTENT) {
+      headers.remove(HttpHeaders.TRANSFER_ENCODING);
+      headers.remove(HttpHeaders.CONTENT_LENGTH);
+    }
+  }
+
   private boolean checkSendHeaders(boolean end) {
     if (!headWritten) {
       if (headersEndHandler != null) {
         headersEndHandler.handle(null);
       }
+      sanitizeHeaders();
       if (Metrics.METRICS_ENABLED && metric != null) {
         conn.metrics().responseBegin(metric, this);
       }
       headWritten = true;
-      headers.status(Integer.toString(statusCode));
+      headers.status(Integer.toString(status.code())); // Could be optimized for usual case ?
       stream.writeHeaders(headers, end);
       if (end) {
         ctx.flush();
@@ -422,7 +425,7 @@ void write(ByteBuf chunk, boolean end) {
         chunk = Unpooled.EMPTY_BUFFER;
       }
       if (end) {
-        if (!headWritten && !headers.contains(HttpHeaderNames.CONTENT_LENGTH)) {
+        if (!headWritten && !head && status != HttpResponseStatus.NOT_MODIFIED && !headers.contains(HttpHeaderNames.CONTENT_LENGTH)) {
           headers().set(HttpHeaderNames.CONTENT_LENGTH, String.valueOf(chunk.readableBytes()));
         }
         handleEnded(false);
diff --git a/src/main/java/io/vertx/core/http/impl/HttpServerResponseImpl.java b/src/main/java/io/vertx/core/http/impl/HttpServerResponseImpl.java
--- a/src/main/java/io/vertx/core/http/impl/HttpServerResponseImpl.java
+++ b/src/main/java/io/vertx/core/http/impl/HttpServerResponseImpl.java
@@ -70,7 +70,6 @@ public class HttpServerResponseImpl implements HttpServerResponse {
   private Handler<Void> endHandler;
   private Handler<Void> headersEndHandler;
   private Handler<Void> bodyEndHandler;
-  private boolean chunked;
   private boolean closed;
   private final VertxHttpHeaders headers;
   private MultiMap trailers;
@@ -137,7 +136,7 @@ public HttpServerResponseImpl setChunked(boolean chunked) {
       checkValid();
       // HTTP 1.0 does not support chunking so we ignore this if HTTP 1.0
       if (version != HttpVersion.HTTP_1_0) {
-        this.chunked = chunked;
+        headers.set(HttpHeaders.TRANSFER_ENCODING, chunked ? "chunked" : null);
       }
       return this;
     }
@@ -146,7 +145,7 @@ public HttpServerResponseImpl setChunked(boolean chunked) {
   @Override
   public boolean isChunked() {
     synchronized (conn) {
-      return chunked;
+      return HttpHeaders.CHUNKED.equals(headers.get(HttpHeaders.TRANSFER_ENCODING));
     }
   }
 
@@ -446,10 +445,10 @@ private void doSendFile(String filename, long offset, long length, Handler<Async
 
       long contentLength = Math.min(length, file.length() - offset);
       bytesWritten = contentLength;
-      if (!headers.contentTypeSet()) {
+      if (!headers.contains(HttpHeaders.CONTENT_TYPE)) {
         String contentType = MimeMapping.getMimeTypeForFilename(filename);
         if (contentType != null) {
-          putHeader(HttpHeaders.CONTENT_TYPE, contentType);
+          headers.set(HttpHeaders.CONTENT_TYPE, contentType);
         }
       }
       prepareHeaders(bytesWritten);
@@ -560,10 +559,14 @@ private void prepareHeaders(long contentLength) {
     } else if (version == HttpVersion.HTTP_1_1 && !keepAlive) {
       headers.set(HttpHeaders.CONNECTION, HttpHeaders.CLOSE);
     }
-    if (!head) {
-      if (chunked) {
-        headers.set(HttpHeaders.TRANSFER_ENCODING, HttpHeaders.CHUNKED);
-      } else if (!headers.contentLengthSet() && contentLength >= 0) {
+    if (head || status == HttpResponseStatus.NOT_MODIFIED) {
+      // For HEAD request or NOT_MODIFIED response
+      // don't set automatically the content-length
+      // and remove the transfer-encoding
+      headers.remove(HttpHeaders.TRANSFER_ENCODING);
+    } else {
+      // Set content-length header automatically
+      if (!headers.contains(HttpHeaders.TRANSFER_ENCODING) && !headers.contains(HttpHeaders.CONTENT_LENGTH) && contentLength >= 0) {
         String value = contentLength == 0 ? "0" : String.valueOf(contentLength);
         headers.set(HttpHeaders.CONTENT_LENGTH, value);
       }
@@ -586,7 +589,7 @@ private void reportResponseBegin() {
   private HttpServerResponseImpl write(ByteBuf chunk) {
     synchronized (conn) {
       checkValid();
-      if (!headWritten && !chunked && !headers.contentLengthSet()) {
+      if (!headWritten && !headers.contains(HttpHeaders.TRANSFER_ENCODING) && !headers.contains(HttpHeaders.CONTENT_LENGTH)) {
         if (version != HttpVersion.HTTP_1_0) {
           throw new IllegalStateException("You must set the Content-Length header to be the total size of the message "
             + "body BEFORE sending any data if you are not using HTTP chunked encoding.");
diff --git a/src/main/java/io/vertx/core/http/impl/VertxHttpResponseEncoder.java b/src/main/java/io/vertx/core/http/impl/VertxHttpResponseEncoder.java
--- a/src/main/java/io/vertx/core/http/impl/VertxHttpResponseEncoder.java
+++ b/src/main/java/io/vertx/core/http/impl/VertxHttpResponseEncoder.java
@@ -42,6 +42,6 @@ public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
   protected boolean isContentAlwaysEmpty(HttpResponse msg) {
     // In HttpServerCodec this is tracked via a FIFO queue of HttpMethod
     // here we track it in the assembled response as we don't use HttpServerCodec
-    return msg instanceof AssembledHttpResponse && ((AssembledHttpResponse) msg).head();
+    return (msg instanceof AssembledHttpResponse && ((AssembledHttpResponse) msg).head()) || super.isContentAlwaysEmpty(msg);
   }
 }
diff --git a/src/main/java/io/vertx/core/http/impl/headers/VertxHttpHeaders.java b/src/main/java/io/vertx/core/http/impl/headers/VertxHttpHeaders.java
--- a/src/main/java/io/vertx/core/http/impl/headers/VertxHttpHeaders.java
+++ b/src/main/java/io/vertx/core/http/impl/headers/VertxHttpHeaders.java
@@ -61,14 +61,6 @@ public VertxHttpHeaders() {
     head.before = head.after = head;
   }
 
-  public boolean contentLengthSet() {
-    return contains(io.vertx.core.http.HttpHeaders.CONTENT_LENGTH);
-  }
-
-  public boolean contentTypeSet() {
-    return contains(io.vertx.core.http.HttpHeaders.CONTENT_TYPE);
-  }
-
   @Override
   public VertxHttpHeaders add(CharSequence name, CharSequence value) {
     int h = AsciiString.hashCode(name);
@@ -383,7 +375,7 @@ public HttpHeaders addShort(CharSequence name, short value) {
 
   @Override
   public HttpHeaders setInt(CharSequence name, int value) {
-    throw new UnsupportedOperationException();
+    return set(name, Integer.toString(value));
   }
 
   @Override
@@ -503,7 +495,10 @@ private VertxHttpHeaders set0(final CharSequence name, final CharSequence strVal
     int h = AsciiString.hashCode(name);
     int i = index(h);
     remove0(h, i, name);
-    add0(h, i, name, strVal);
+    if (strVal != null) {
+      // test me
+      add0(h, i, name, strVal);
+    }
     return this;
   }
 


Test patch:
diff --git a/src/test/java/io/vertx/core/http/Http1xTest.java b/src/test/java/io/vertx/core/http/Http1xTest.java
--- a/src/test/java/io/vertx/core/http/Http1xTest.java
+++ b/src/test/java/io/vertx/core/http/Http1xTest.java
@@ -3835,66 +3835,55 @@ public void testSendFileFailsWhenClientClosesConnection() throws Exception {
     await();
   }
 
-  @Test
-  public void testHeadMustNotAutomaticallySetContentHeaders() throws Exception {
-    testHeadAutomaticallySet(MultiMap.caseInsensitiveMultiMap(), respHeaders -> {
-      assertFalse(respHeaders.contains("Content-Length"));
-      assertFalse(respHeaders.contains("Transfer-Encoding"));
-    });
-  }
-
-  @Test
-  public void testHeadMustNotSendBodyWhenContentLengthSet() throws Exception {
-    MultiMap reqHeaders = MultiMap.caseInsensitiveMultiMap();
-    reqHeaders.set("Content-Length", "10");
-    testHeadAutomaticallySet(reqHeaders, respHeaders -> {
-      assertEquals(" 10", respHeaders.get("Content-Length"));
-      assertNull(respHeaders.get("Transfer-Encoding"));
-    });
-  }
-
-  @Test
-  public void testHeadMustNotSendBodyWhenTransferEncodingSet() throws Exception {
-    MultiMap reqHeaders = MultiMap.caseInsensitiveMultiMap();
-    reqHeaders.set("Transfer-Encoding", "chunked");
-    testHeadAutomaticallySet(reqHeaders, respHeaders -> {
-      assertNull(respHeaders.get("Content-Length"));
-      assertEquals(" chunked", respHeaders.get("Transfer-Encoding"));
-    });
-  }
-
-  private void testHeadAutomaticallySet(MultiMap reqHeaders, Consumer<MultiMap> headersChecker) throws Exception {
+  // Use a raw socket to check the body response is effectively empty (it could be an empty chunk)
+  protected MultiMap checkEmptyHttpResponse(HttpMethod method, int sc, MultiMap reqHeaders) throws Exception {
     server.requestHandler(req -> {
       HttpServerResponse resp = req.response();
+      resp.setStatusCode(sc);
       resp.headers().addAll(reqHeaders);
       resp.end();
     });
     startServer();
     NetClient client = vertx.createNetClient();
-    client.connect(DEFAULT_HTTP_PORT, DEFAULT_HTTPS_HOST, onSuccess(so -> {
-      so.write(
-          "HEAD / HTTP/1.1\r\n" +
-          "Connection: close\r\n" +
-          "\r\n");
-      Buffer buff = Buffer.buffer();
-      so.handler(buff::appendBuffer);
-      so.endHandler(v -> {
-        String content = buff.toString();
-        int idx = content.indexOf("\r\n\r\n");
-        LinkedList<String> records = new LinkedList<String>(Arrays.asList(content.substring(0, idx).split("\\r\\n")));
-        assertEquals("HTTP/1.1 200 OK", records.removeFirst());
-        assertEquals("", content.substring(idx + 4));
-        MultiMap respHeaders = MultiMap.caseInsensitiveMultiMap();
-        records.forEach(record -> {
-          int index = record.indexOf(":");
-          String value = record.substring(0, index);
-          respHeaders.add(value, record.substring(index + 1));
-        });
-        headersChecker.accept(respHeaders);
-        testComplete();
+    try {
+      CompletableFuture<MultiMap> result = new CompletableFuture<>();
+      client.connect(DEFAULT_HTTP_PORT, DEFAULT_HTTPS_HOST, ar -> {
+        if (ar.succeeded()) {
+          NetSocket so = ar.result();
+          so.write(
+            method.name() + " / HTTP/1.1\r\n" +
+              "Connection: close\r\n" +
+              "\r\n");
+          Buffer body = Buffer.buffer();
+          so.exceptionHandler(result::completeExceptionally);
+          so.handler(body::appendBuffer);
+          so.endHandler(v -> {
+            String content = body.toString();
+            int idx = content.indexOf("\r\n\r\n");
+            if (idx == content.length() - 4) {
+              LinkedList<String> records = new LinkedList<>(Arrays.asList(content.substring(0, idx).split("\\r\\n")));
+              String statusLine = records.removeFirst();
+              assertEquals("HTTP/1.1 " + sc, statusLine.substring(0, statusLine.indexOf(' ', 9)));
+              assertEquals("", content.substring(idx + 4));
+              MultiMap respHeaders = MultiMap.caseInsensitiveMultiMap();
+              records.forEach(record -> {
+                int index = record.indexOf(":");
+                String value = record.substring(0, index);
+                respHeaders.add(value.trim(), record.substring(index + 1).trim());
+              });
+              result.complete(respHeaders);
+            } else {
+              result.completeExceptionally(new Exception());
+            }
+          });
+        } else {
+          result.completeExceptionally(ar.cause());
+        }
       });
-    }));
-    await();
+      return result.get(20, TimeUnit.SECONDS);
+    } finally {
+      client.close();
+    }
   }
 
   @Test
diff --git a/src/test/java/io/vertx/core/http/HttpTest.java b/src/test/java/io/vertx/core/http/HttpTest.java
--- a/src/test/java/io/vertx/core/http/HttpTest.java
+++ b/src/test/java/io/vertx/core/http/HttpTest.java
@@ -2320,7 +2320,7 @@ public void testListenTwice2() throws Exception {
   }
 
   @Test
-  public void testHeadNoBody() {
+  public void testHeadCanSetContentLength() {
     server.requestHandler(req -> {
       assertEquals(HttpMethod.HEAD, req.method());
       // Head never contains a body but it can contain a Content-Length header
@@ -2331,7 +2331,158 @@ public void testHeadNoBody() {
 
     server.listen(onSuccess(s -> {
       client.request(HttpMethod.HEAD, DEFAULT_HTTP_PORT, DEFAULT_HTTP_HOST, DEFAULT_TEST_URI, resp -> {
-        assertEquals(41, Integer.parseInt(resp.headers().get("Content-Length")));
+        assertEquals("41", resp.headers().get("Content-Length"));
+        resp.endHandler(v -> testComplete());
+      }).end();
+    }));
+
+    await();
+  }
+
+  @Test
+  public void testHeadDoesNotSetAutomaticallySetContentLengthHeader() throws Exception {
+    MultiMap respHeaders = checkEmptyHttpResponse(HttpMethod.HEAD, 200, MultiMap.caseInsensitiveMultiMap());
+    assertNull(respHeaders.get("content-length"));
+    assertNull(respHeaders.get("transfer-encoding"));
+  }
+
+  @Test
+  public void testHeadAllowsContentLengthHeader() throws Exception {
+    MultiMap respHeaders = checkEmptyHttpResponse(HttpMethod.HEAD, 200, MultiMap.caseInsensitiveMultiMap().set("content-length", "34"));
+    assertEquals("34", respHeaders.get("content-length"));
+    assertNull(respHeaders.get("transfer-encoding"));
+  }
+
+  @Test
+  public void testHeadRemovesTransferEncodingHeader() throws Exception {
+    MultiMap respHeaders = checkEmptyHttpResponse(HttpMethod.HEAD, 200, MultiMap.caseInsensitiveMultiMap().set("transfer-encoding", "chunked"));
+    assertNull(respHeaders.get("content-length"));
+    assertNull(respHeaders.get("transfer-encoding"));
+  }
+
+  @Test
+  public void testNoContentRemovesContentLengthHeader() throws Exception {
+    MultiMap respHeaders = checkEmptyHttpResponse(HttpMethod.GET, 204, MultiMap.caseInsensitiveMultiMap().set("content-length", "34"));
+    assertNull(respHeaders.get("content-length"));
+    assertNull(respHeaders.get("transfer-encoding"));
+  }
+
+  @Test
+  public void testNoContentRemovesTransferEncodingHeader() throws Exception {
+    MultiMap respHeaders = checkEmptyHttpResponse(HttpMethod.GET, 204, MultiMap.caseInsensitiveMultiMap().set("transfer-encoding", "chunked"));
+    assertNull(respHeaders.get("content-length"));
+    assertNull(respHeaders.get("transfer-encoding"));
+  }
+
+  @Test
+  public void testResetContentSetsContentLengthHeader() throws Exception {
+    MultiMap respHeaders = checkEmptyHttpResponse(HttpMethod.GET, 205, MultiMap.caseInsensitiveMultiMap());
+    assertEquals("0", respHeaders.get("content-length"));
+    assertNull(respHeaders.get("transfer-encoding"));
+  }
+
+  @Test
+  public void testResetContentRemovesTransferEncodingHeader() throws Exception {
+    MultiMap respHeaders = checkEmptyHttpResponse(HttpMethod.GET, 205, MultiMap.caseInsensitiveMultiMap().set("transfer-encoding", "chunked"));
+    assertEquals("0", respHeaders.get("content-length"));
+    assertNull(respHeaders.get("transfer-encoding"));
+  }
+
+  @Test
+  public void testNotModifiedDoesNotSetAutomaticallySetContentLengthHeader() throws Exception {
+    MultiMap respHeaders = checkEmptyHttpResponse(HttpMethod.GET, 304, MultiMap.caseInsensitiveMultiMap());
+    assertNull(respHeaders.get("content-length"));
+    assertNull(respHeaders.get("transfer-encoding"));
+  }
+
+  @Test
+  public void testNotModifiedAllowsContentLengthHeader() throws Exception {
+    MultiMap respHeaders = checkEmptyHttpResponse(HttpMethod.GET, 304, MultiMap.caseInsensitiveMultiMap().set("content-length", "34"));
+    assertEquals("34", respHeaders.get("Content-Length"));
+    assertNull(respHeaders.get("transfer-encoding"));
+  }
+
+  @Test
+  public void testNotModifiedRemovesTransferEncodingHeader() throws Exception {
+    MultiMap respHeaders = checkEmptyHttpResponse(HttpMethod.GET, 304, MultiMap.caseInsensitiveMultiMap().set("transfer-encoding", "chunked"));
+    assertNull(respHeaders.get("content-length"));
+    assertNull(respHeaders.get("transfer-encoding"));
+  }
+
+  @Test
+  public void test1xxRemovesContentLengthHeader() throws Exception {
+    MultiMap respHeaders = checkEmptyHttpResponse(HttpMethod.GET, 102, MultiMap.caseInsensitiveMultiMap().set("content-length", "34"));
+    assertNull(respHeaders.get("content-length"));
+    assertNull(respHeaders.get("transfer-encoding"));
+  }
+
+  @Test
+  public void test1xxRemovesTransferEncodingHeader() throws Exception {
+    MultiMap respHeaders = checkEmptyHttpResponse(HttpMethod.GET, 102, MultiMap.caseInsensitiveMultiMap().set("transfer-encoding", "chunked"));
+    assertNull(respHeaders.get("content-length"));
+    assertNull(respHeaders.get("transfer-encoding"));
+  }
+
+  protected MultiMap checkEmptyHttpResponse(HttpMethod method, int sc, MultiMap reqHeaders) throws Exception {
+    server.requestHandler(req -> {
+      HttpServerResponse resp = req.response();
+      resp.setStatusCode(sc);
+      resp.headers().addAll(reqHeaders);
+      resp.end();
+    });
+    startServer();
+    try {
+      CompletableFuture<MultiMap> result = new CompletableFuture<>();
+      client.request(method, DEFAULT_HTTP_PORT, DEFAULT_HTTPS_HOST, "/", resp -> {
+        Buffer body = Buffer.buffer();
+        resp.exceptionHandler(result::completeExceptionally);
+        resp.handler(body::appendBuffer);
+        resp.endHandler(v -> {
+          if (body.length() > 0) {
+            result.completeExceptionally(new Exception());
+          } else {
+            result.complete(resp.headers());
+          }
+        });
+      }).setFollowRedirects(false)
+        .exceptionHandler(result::completeExceptionally)
+        .end();
+      return result.get(20, TimeUnit.SECONDS);
+    } finally {
+      client.close();
+    }
+  }
+
+  @Test
+  public void testHeadHasNoContentLengthByDefault() {
+    server.requestHandler(req -> {
+      assertEquals(HttpMethod.HEAD, req.method());
+      // By default HEAD does not have a content-length header
+      req.response().end();
+    });
+
+    server.listen(onSuccess(s -> {
+      client.request(HttpMethod.HEAD, DEFAULT_HTTP_PORT, DEFAULT_HTTP_HOST, DEFAULT_TEST_URI, resp -> {
+        assertNull(resp.headers().get(HttpHeaders.CONTENT_LENGTH));
+        resp.endHandler(v -> testComplete());
+      }).end();
+    }));
+
+    await();
+  }
+
+  @Test
+  public void testHeadButCanSetContentLength() {
+    server.requestHandler(req -> {
+      assertEquals(HttpMethod.HEAD, req.method());
+      // By default HEAD does not have a content-length header but it can contain a content-length header
+      // if explicitly set
+      req.response().putHeader(HttpHeaders.CONTENT_LENGTH, "41").end();
+    });
+
+    server.listen(onSuccess(s -> {
+      client.request(HttpMethod.HEAD, DEFAULT_HTTP_PORT, DEFAULT_HTTP_HOST, DEFAULT_TEST_URI, resp -> {
+        assertEquals("41", resp.headers().get(HttpHeaders.CONTENT_LENGTH));
         resp.endHandler(v -> testComplete());
       }).end();
     }));