When I use Undertow as server , and make a range request, the response will have two "Content-Length" header.
request
GET /inspection/api/v1/binary/1568 HTTP/1.1 Host: inspection.lubansoft.net:58888 Connection: keep-alive Pragma: no-cache Cache-Control: no-cache User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 Accept: / Accept-Encoding: identity Accept-Language: zh-CN,zh;q=0.9 Range: bytes=0-65535
response
HTTP/1.1 206 Partial Content Expires: 0 Cache-Control: no-cache, no-store, max-age=0, must-revalidate X-XSS-Protection: 1; mode=block Pragma: no-cache Content-Disposition: form-data; name="attachment"; filename*=UTF-8''%E8%AF%84%E5%AE%9A%E8%A1%A810.3.2%20%E6%98%8E%E6%B4%9E%E6%B5%87%E7%AD%91%E5%88%86%E9%A1%B9%E5%B7%A5%E7%A8%8B%E8%B4%A8%E9%87%8F%E6%A3%80%E9%AA%8C%E8%AF%84%E5%AE%9A%E8%A1%A8%28SG%29.pdf Accept-Ranges: bytes Date: Mon, 21 Dec 2020 13:02:45 GMT Connection: keep-alive Content-Language: zh-CN X-Frame-Options: SAMEORIGIN ETag: "10" Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers Last-Modified: Mon, 21 Dec 2020 06:02:31 GMT Content-Range: bytes 196608-212611/212612 X-Content-Type-Options: nosniff Content-Length: 212612 Content-Length: 16004 Content-Type: application/pdf
in response header, Content-Length has two values , and chrome will have net::ERR_RESPONSE_HEADERS_MULTIPLE_CONTENT_LENGTH error
org.springframework.web.servlet.resource.ResourceHttpRequestHandler
actually writes the content-length twice. Once, with the entire content-length and then later on, if a partial-content response, with the range length.
Tomcat handles this just fine accepting the last content-length set as the authoritative content-length and writes it back to the client.
Undertow seems to assume all headers are multi-value and writes both content-length headers back to the client.
Comment From: lmtoo
https://github.com/paulcwarren/spring-content/issues/449
Comment From: paulcwarren
The http spec doesn’t specify how to handle multiple content-length headers with different values. This SO post, commented on by spec author Julian Reschke, confirms that. But perhaps that is because it is obviously an error?
The only think I can really determine from the spec is that is it meant to be a single-valued header. So, probably this can be chased down to the stack to undertow using tomcat's behavior as justification. They could decide to treat the last set header as the authoritative value (as tomcat does) and everything would be fine, but equally they could decide to treat it as an error I guess? Either way, maybe this is the best first course of action.
But I also wonder if org.springframework.web.servlet.resource.ResourceHttpRequestHandler.setHeaders
has to write a content-length header at all? Given, content-length is single-valued and the message converter attempts to write the header anyway. Perhaps it is too assumptive to knowingly write this “provisionally” instance up-front?
Would this be viewed as a backward incompatible change though? A custom ResourceHttpMessageConverter
might rely on the original setHeaders
method. However, to do so I think it would have to override getContentLength
to return null which would be weird (why would a resource not want to return a content-length???). So, perhaps it is ok?
What other considerations would we need to make?
Comment From: quaff
I think it should be fixed in Undertow not Spring.
Comment From: bclozel
I've reproduced the problem and looked into it.
Here's what's happening:
- the
ResourceHttpRequestHandler
is setting the content length usingServletHttpResponse.setContentLength
orServletHttpResponse.setContentLengthLong
- it's then delegating to the message converters.
ResourceHttpMessageConverter
andResourceRegionHttpMessageConverter
are setting the content length usingHttpHeaders.setContentLength
(this adds a "Content-Length" header to the header map). - later,
ServletServerHttpResponse
is writing headers to the Servlet response by callingServletResponse.addHeader
for each entry - unlike other containers, Undertow doesn't seem to enforce a single Content-Length header if we're using
ServletHttpResponse.addHeader
to add headers, which is the case inServletServerHttpResponse
Because the "Content-Length" header can be set in multiple ways through the Servlet API, containers behave differently. It seems that Tomcat is enforcing a single response header no matter what, while Undertow is enforcing that only if it's set through the setContentLength{Long}
methods.
We can fix this problem with a couple of changes:
- first, remove the duplicate code and avoid setting the content length twice
- then, align
org.springframework.http.server.ServletServerHttpResponse
withorg.springframework.http.server.reactive.ServletServerHttpResponse
and callServletHttpResponse.setContentLength
after copying the headers from the map.
Note that those specific Servlet containers behavior are hard to test without complete integration tests or replicating their headers implementation. Our own org.springframework.mock.web.MockHttpServletResponse
is enforcing a single Content-Length header no matter what.