Spring-Boot Version: 1.5.22.RELEASE

When creating a route with the method HEAD in spring-boot, i.e.

@RequestMapping(value = "/", method = HEAD)

And you have chunked-encoding enabled by default, it doesn't appear to be possible to return a response that is completely empty (which is required for HEAD requests). For example if you do something like this

return ResponseEntity.status(OK).contentLength(0).build();

And analyze the bytes in the bodies response you get "0\r\n\r\n" which indicates chunked encoding. This is illegal according to the RFC-spec, see https://tools.ietf.org/html/rfc7230#section-3.3 specifically

   Responses to the HEAD request method (Section 4.3.2
   of [RFC7231]) never include a message body because the associated
   response header fields (e.g., Transfer-Encoding, Content-Length,
   etc.), if present, indicate only what their values would have been if
   the request method had been GET (Section 4.3.1 of [RFC7231]).

In other words, the response body should be completely empty. This is causing issues with certain http clients such as akka-http client which expect there to be no response in the body when making a HEAD request. The related issue with akka-http can be seen here https://github.com/akka/akka-http/issues/2883#issuecomment-574070860

Comment From: rstoyanchev

I can't reproduce this. Please, provide a sample and the curl command you use.

Note it is unusual to see an explicit mapping to HEAD which is transparently supported. I'd expect normally to see the actual GET method getting invoked to produce the would-be response, which is then omitted due to doHead of HttpServlet.

Content-Length and Transfer-Encoding are mutually exclusive, so it's not very clear how this scenario relates to chunked encoding if the content-length is set explicitly. The HEAD should be returning the exact headers as GET. In any case Spring does not set Transfer-Encoding explicitly, nor does it render chunked encoding. That's done at the server level.

Comment From: mdedetrich

So to be clear, the issue with the response of the HEAD request is not in the headers but in the body, i.e. quoting @jrudolph who diagnosed the issue here https://github.com/akka/akka-http/issues/2883

2020-01-13 17:57:25,086 [DEBUG] [flow_id=] Materializer -
[client-plain-text FromNet] Element: SessionBytes ByteString(284
bytes)
48 54 54 50 2F 31 2E 31 20 34 30 34 20 0D 0A 43 | HTTP/1.1 404 ..C
61 63 68 65 2D 43 6F 6E 74 72 6F 6C 3A 20 6E 6F | ache-Control: no
2D 63 61 63 68 65 2C 20 6E 6F 2D 73 74 6F 72 65 | -cache, no-store
2C 20 6D 61 78 2D 61 67 65 3D 30 2C 20 6D 75 73 | , max-age=0, mus
74 2D 72 65 76 61 6C 69 64 61 74 65 0D 0A 43 6F | t-revalidate..Co
6E 74 65 6E 74 2D 4C 65 6E 67 74 68 3A 20 30 0D | ntent-Length: 0.
0A 44 61 74 65 3A 20 4D 6F 6E 2C 20 31 33 20 4A | .Date: Mon, 13 J
61 6E 20 32 30 32 30 20 31 36 3A 35 37 3A 32 34 | an 2020 16:57:24
20 47 4D 54 0D 0A 45 78 70 69 72 65 73 3A 20 30 | GMT..Expires: 0
0D 0A 50 72 61 67 6D 61 3A 20 6E 6F 2D 63 61 63 | ..Pragma: no-cac
68 65 0D 0A 58 2D 43 6F 6E 74 65 6E 74 2D 54 79 | he..X-Content-Ty
70 65 2D 4F 70 74 69 6F 6E 73 3A 20 6E 6F 73 6E | pe-Options: nosn
69 66 66 0D 0A 58 2D 46 72 61 6D 65 2D 4F 70 74 | iff..X-Frame-Opt
69 6F 6E 73 3A 20 44 45 4E 59 0D 0A 58 2D 58 53 | ions: DENY..X-XS
53 2D 50 72 6F 74 65 63 74 69 6F 6E 3A 20 31 3B | S-Protection: 1;
20 6D 6F 64 65 3D 62 6C 6F 63 6B 0D 0A 43 6F 6E | mode=block..Con
6E 65 63 74 69 6F 6E 3A 20 6B 65 65 70 2D 61 6C | nection: keep-al
69 76 65 0D 0A 0D 0A 30 0D 0A 0D 0A | ive....0....

That's an invalid HEAD response. It has a Content-Length: 0 header but also a body that looks like empty Transfer-Encoding: chunked content. That seems like a spring-boot issue.

i.e. The response body of a HEAD request is not meant to have any body at all and in the case of spring, its putting empty Transfer-Encoding: chunked content in there. Note you have to be careful when using curl to debug these issues since curl will by default just omit printing any body of a HEAD request (even if one exists) since HEAD is not meant to have any body at all. You can try using the --raw command to see if it helps but the best way to debug this is looking at the bytes of the response (as seen in the example above).

Would it be more useful to create reproduction repo for you? It would be a docker container with a spring webserver and a simple akka-http client making requests against it.

Note it is unusual to see an explicit mapping to HEAD which is transparently supported. I'd expect normally to see the actual GET method getting invoked to produce the would-be response, which is then omitted due to doHead of HttpServlet.

I am not sure what you mean by transparently supported, but in our case we have a route that looks like this (simplified)

@RequestMapping(value = "/{item}", method = HEAD)
public ResponseEntity<Void> head(@PathVariable final String item) {
    final Boolean exists = itemRepository.exists(item);
    if (!exists) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).contentLength(0).build();
    } else {
        return ResponseEntity.status(OK).contentLength(0).build();
    }
}

One of the points of using the HEAD method is as a fast way of checking if an entity exists. Since the HEAD doesn't return a body, an implementation of a HEAD route can be optimized to be very fast since you are only checking the existence of an entity (indicated here by final Boolean exists = itemRepository.exists(item);). This is in contrast to GET, which forces the server to return all of the content of the entity in the response body (even though in this case we don't need it).

In summary the problem here seems to be that I can't actually generate a ResponseEntity that has a completely empty response body to make a valid HEAD response, i.e. doesn't contain empty Chunked-Encoding content.

Hope that clears things up

Comment From: rstoyanchev

These are the logs I see:

         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 45 41 44 20 2f 32 34 33 35 33 20 48 54 54 50 |HEAD /24353 HTTP|
|00000010| 2f 31 2e 31 0d 0a 75 73 65 72 2d 61 67 65 6e 74 |/1.1..user-agent|
|00000020| 3a 20 52 65 61 63 74 6f 72 4e 65 74 74 79 2f 30 |: ReactorNetty/0|
|00000030| 2e 39 2e 32 2e 52 45 4c 45 41 53 45 0d 0a 68 6f |.9.2.RELEASE..ho|
|00000040| 73 74 3a 20 6c 6f 63 61 6c 68 6f 73 74 3a 38 30 |st: localhost:80|
|00000050| 38 30 0d 0a 61 63 63 65 70 74 3a 20 2a 2f 2a 0d |80..accept: */*.|
|00000060| 0a 0d 0a                                        |...             |
+--------+-------------------------------------------------+----------------+
10:07:15.416 [reactor-http-epoll-1] DEBUG reactor.netty.http.client.HttpClient - [id: 0x572eac22, L:/127.0.0.1:50848 - R:localhost/127.0.0.1:8080] FLUSH
10:07:15.421 [reactor-http-epoll-1] DEBUG reactor.netty.http.client.HttpClient - [id: 0x572eac22, L:/127.0.0.1:50848 - R:localhost/127.0.0.1:8080] WRITE: 0B
10:07:15.421 [reactor-http-epoll-1] DEBUG reactor.netty.http.client.HttpClient - [id: 0x572eac22, L:/127.0.0.1:50848 - R:localhost/127.0.0.1:8080] FLUSH
10:07:15.422 [reactor-http-epoll-1] DEBUG reactor.netty.resources.PooledConnectionProvider - [id: 0x572eac22, L:/127.0.0.1:50848 - R:localhost/127.0.0.1:8080] onStateChange(HEAD{uri=/24353, connection=PooledConnection{channel=[id: 0x572eac22, L:/127.0.0.1:50848 - R:localhost/127.0.0.1:8080]}}, [request_sent])
10:07:15.439 [reactor-http-epoll-1] DEBUG reactor.netty.http.client.HttpClient - [id: 0x572eac22, L:/127.0.0.1:50848 - R:localhost/127.0.0.1:8080] READ: 73B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 54 54 50 2f 31 2e 31 20 32 30 30 20 0d 0a 43 |HTTP/1.1 200 ..C|
|00000010| 6f 6e 74 65 6e 74 2d 4c 65 6e 67 74 68 3a 20 30 |ontent-Length: 0|
|00000020| 0d 0a 44 61 74 65 3a 20 57 65 64 2c 20 31 35 20 |..Date: Wed, 15 |
|00000030| 4a 61 6e 20 32 30 32 30 20 31 30 3a 30 37 3a 31 |Jan 2020 10:07:1|
|00000040| 35 20 47 4d 54 0d 0a 0d 0a                      |5 GMT....       |
+--------+-------------------------------------------------+----------------+

As I said I can't reproduce the issue.

Please, provide a server sample. No akka and preferably no dokker, just the server piece that produces an incorrect response. In other words an isolated sample.

Comment From: rstoyanchev

and in the case of spring, its putting empty Transfer-Encoding: chunked content in there

That looks like a conclusion ahead of evidence.

This issue, whatever the root cause, is unlikely to have much to do with Spring MVC. The support for HTTP HEAD is built into the Servlet container. I gave you a pointer already to HttpServlet#doHead. Further no code anywhere in the Spring Framework does the actual chunked encoding. Tt's something at the level of Tomcat or whatever server your'e using. This is why I asked for a something I can run but the root cause is unlikely to be anything in the Spring Framework.

Comment From: mdedetrich

This issue, whatever the root cause, is unlikely to have much to do with Spring MVC. The support for HTTP HEAD is built into the Servlet container. I gave you a pointer already to HttpServlet#doHead. Further no code anywhere in the Spring Framework does the actual chunked encoding. That's something at the level of Tomcat or whatever server your'e using. This is why I asked for a something I can run but the root cause is unlikely to be anything in the Spring Framework.

Okay thanks, I actually already started looking into the reverse proxy/load balancer that is happening over our network to to see if the problem is elsewhere.

Comment From: mdedetrich

This may be related, investigating https://forums.aws.amazon.com/thread.jspa?threadID=26610

Comment From: spring-projects-issues

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

Comment From: mdedetrich

So I just ran the app locally and I can confirm that its not spring-framework (by itself) that is adding these empty responses into the chunked body.

I will re-open the ticket if I find something to the contrary.