Users of applications running on Tanzu Application Service (TAS) for VMs may experience unexpected 502 errors. Upon looking into /var/vcap/sys/log/gorouter/gorouter.stdout.log, you will see "too many transfer encodings" in the error message. This happens after upgrading to TAS for VMs or Isolation Segment.
Routing Release 0.209.0+
A recent issue was discovered in later releases of TAS for VMs where some versions of TAS will silently remove the Transfer encoding header and replace it with a content length header. This leads to a false sense of mitigation and we recommend reviewing this related issue. For more information, refer to Clients receive responses with no Content-Length header and a chunked encoded body after upgrading Tanzu Application Service for VMs.
The following is an example of the error message you receive:
"error":"net/http: HTTP/1.x transport connection broken: too many transfer encodings: [\"chunked\" \"chunked\"]
An application may add "Transfer-Encoding: chunked" or other transfer encoding headers. However, the need to do this is rare as most often your web server, a library, or a framework is going to do this automatically, as necessary, for you. The error described in this KB occurs if any response from an application has multiple transfer-encoding headers, either directly adding or a framework adds on its behalf, or a single transfer-encoding header with a value other than "chunked". For the case of multiple headers, it does not matter if there are two identical headers with the same value or if there are two transfer-encoding headers with different values.
The reason the error listed above is happening in the specific releases mentioned above is that starting with these releases Gorouter has been built with Golang 1.15. Before Golang 1.15, duplicated transfer-encoding headers or headers with two values were permitted by Golang's proxy roundTrip. However, starting in Golang 1.15, the implementation is more strict. Specifically, the router is now rejecting responses from applications with either multiple transfer encodings or out-of-spec "identity" encoding values. Golang 1.15 is more strict about the Transfer Encoding header to prevent a well-known request smuggling vulnerability, as well as mitigate a broad class of security issues around this header over time.
This can happen under a number of scenarios. The following is a list of the scenarios that VMware Tanzu Support has specifically observed causing this error:
Detecting and monitoring this issue is complex and requires an upgrade to TAS versions 2.7.31, 2.8.25, 2.9.19, and 2.10.11, or Isolation Segment 2.7.30, 2.8.24, 2.9.18, 2.10.10. This process has been further explained and in the KB titled How to detect applications that return multiple transfer-encoding headers.
If you created a route service based on this example code, https://github.com/nebhale/route-service-example (no longer published due to this bug), you will be impacted as the example code was copying all response headers, include transfer-encoding headers from the proxied response to the client response (see the bold line below).
return this.webClient
.method(request.getMethod())
.uri(forwardedUrl)
.headers(headers -> headers.putAll(forwardedHttpHeaders))
.body((outputMessage, context) -> outputMessage.writeWith(request.getBody()))
.exchange()
.map(response -> {
this.logger.info("Outgoing Response: {}", formatResponse(response.statusCode(), response.headers().asHttpHeaders()));
return ResponseEntity
.status(response.statusCode())
.headers(response.headers().asHttpHeaders())
.body(response.bodyToFlux(DataBuffer.class));
});
To implement correctly, you will need to strip out the transfer-encoding header, like in this example.
return this.webClient
.method(request.getMethod())
.uri(forwardedUrl)
.headers(headers -> headers.putAll(forwardedHttpHeaders))
.body((outputMessage, context) -> outputMessage.writeWith(request.getBody()))
.exchange()
.map(response -> {
HttpHeaders headers = getResponseHeaders(response.headers().asHttpHeaders());
this.logger.info("Outgoing Response: {}", formatResponse(response.statusCode(), headers));
return ResponseEntity
.status(response.statusCode())
.headers(headers)
.body(response.bodyToFlux(DataBuffer.class));
});
}
Where the getResponseHeaders function looks like this:
private HttpHeaders getResponseHeaders(HttpHeaders headers) {
return headers.entrySet().stream()
.filter(entry -> !entry.getKey().equalsIgnoreCase(TRANSFER_ENCODING))
.collect(HttpHeaders::new, (httpHeaders, entry) -> httpHeaders.addAll(entry.getKey(), entry.getValue()), HttpHeaders::putAll);
}
As an additional note beyond the context of this issue, it is not good practice to copy every header when proxying traffic. The example above strips out a few request headers and the transfer-encoding header on the response, but you would want to be more restrictive for production applications to prohibit clients from sending headers to manipulate backend applications in ways they should not be able to do (like the HTTPoxy vulnerability) and to prohibit information, like CORS headers, from leaking out of backend services to clients.