Bug 61086

Summary: Some clients hang when HTTP responses give status 205 Reset Content
Product: Tomcat 8 Reporter: Mayeul <mayeul.marguet>
Component: ConnectorsAssignee: Tomcat Developers Mailing List <dev>
Severity: normal CC: frederic.giltay, mthur
Priority: P2    
Version: 8.5.15   
Target Milestone: ----   
Hardware: All   
OS: All   
Attachments: Exemple standalone servlet to give out HTTP 205 response
Naive patch to remove 205 from the status without content
Patch 0-length content for 205 status
standalone application with embedded tomcat

Description Mayeul 2017-05-11 20:29:50 UTC
Created attachment 34992 [details]
Exemple standalone servlet to give out HTTP 205 response

When a servlet running on Tomcat sends a response over HTTP with status 205 Reset Content, some clients hang with this response and just wait for it to "complete" after Tomcat considers it fully done.

So far I've identified two clients:
- command line program curl, version 7.52.1,
- Jersey client, version 1.19.1.

Using Tomcat 8.5.15 (latest release), but the issue was here for as long as I went back and it seems still here in Tomcat 9.

Debugging the HTTP communication shows it has to do with the fact that the response has no body (which is correct, as mandated by RFC for status 205), and no indication of content length to explicitly say that there is no body. That last part is incorrect behavior according to RFC 7231 section 6.3.6:

   " Since the 205 status code implies that no additional content will be provided, a server MUST NOT generate a payload in a 205 response.  In other words, a server MUST do one of the following for a 205 response: a) indicate a zero-length body for the response by including a Content-Length header field with a value of 0; b) indicate a zero-length payload for the response by including a Transfer-Encoding header field with a value of chunked and a message body consisting of a single chunk of zero-length; or, c) close the connection immediately after sending the blank line terminating the header section. "

It seems the HTTP clients I've identified, do rely on this requirement stated by RFC. Testing with servers that do add a Content-Length: 0 header or a Transfer-encoding chunked with a zero-length chunk with a status 205, these clients behave as expected. Also note, that Tomcat will typically eventually reach its keep-alive timeout and close the connection. Which is actually a valid way to end the response, and these clients do accept it when they don't reach their own timeouts. It's just the response takes by default 20 seconds to be finished, and is done with closing a perfectly re-usable connection.

Steps to reproduce:

 (1) Have a clean Tomcat install version 8.5.15

 (2) Deploy on it a root webapp that responds to requests with
     HTTP status 205.

     You can use the standalone servlet class I put in attachment.
     As can be seen, it responds to all requests with status 205,
     and it adds a custom header just to be sure the response comes
     from this servlet.

 (3) Make an HTTP request to it with curl.

     Response looks like:

$ curl -v http://localhost:8080
* STATE: INIT => CONNECT handle 0x6000578f0; line 1413 (connection #-5000)
* Rebuilt URL to: http://localhost:8080/
* Added connection 0. The cache now contains 1 members
*   Trying
* STATE: CONNECT => WAITCONNECT handle 0x6000578f0; line 1466 (connection #0)
* Connected to localhost ( port 8080 (#0)
* STATE: WAITCONNECT => SENDPROTOCONNECT handle 0x6000578f0; line 1583 (connection #0)
* Marked for [keep alive]: HTTP default
* STATE: SENDPROTOCONNECT => DO handle 0x6000578f0; line 1601 (connection #0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.52.1
> Accept: */*
* STATE: DO => DO_DONE handle 0x6000578f0; line 1680 (connection #0)
* STATE: DO_DONE => WAITPERFORM handle 0x6000578f0; line 1807 (connection #0)
* STATE: WAITPERFORM => PERFORM handle 0x6000578f0; line 1817 (connection #0)
* HTTP 1.1 or later with persistent connection, pipelining supported
< HTTP/1.1 205
< x-mmar-servletname: return205
< Date: Thu, 11 May 2017 15:43:26 GMT
* no chunk, no close, no size. Assume close to signal end
* Marked for [closure]: HTTP: No end-of-message indicator
* STATE: PERFORM => DONE handle 0x6000578f0; line 1981 (connection #0)
* multi_done
* Curl_http_done: called premature == 0
* Closing connection 0
* The cache now contains 0 members

     curl hangs for a while after "Marked for [closure]: HTTP: No end-of-message indicator".
     Then after 20 seconds Tomcat reaches connection
     keep-alive timeout, closes the connection and curl
     accepts it as a valid way to finish the response.

Proposed (naive) patch:

I have located the cause for this behavior, in class
in line 1144.
Status 205 is treated the same way as 204 and 304,
that is to say no body as mandated by RFC,
but also no content length information.

The naive patch attached just removes 205 from those,
which solves the issue with the problematic clients.
However it makes it possible to add a body to a
205 response, and it becomes the webapp's author's
responsibility to not do that.

Another, possibly better, approach, could be to
have a special case for 205 only, where it
would ignore any attempt to put a content,
but it would add the header Content-Length: 0.
Comment 1 Mayeul 2017-05-11 20:31:42 UTC
Created attachment 34993 [details]
Naive patch to remove 205 from the status without content
Comment 2 Mark Thomas 2017-05-16 11:12:31 UTC
Thanks for the report.

This has been fixed by explicitly setting content length to zero for 205 responses.

This has been fixed in:
- 9.0.x for 9.0.0.M22 onwards
- 8.5.x for 8.5.16 onwards
- 8.0.x for 8.0.45 onwards
- 7.0.x for 7.0.79 onwards
Comment 3 Alexandr Saperov 2017-07-26 09:35:57 UTC
Still reproducable on 8.5.16-19.

1129 entityBody = false;
1130 contentDelimitation = true;
1131 if (statusCode == 205) {
1132    // RFC 7231 requires the server to explicitly signal an empty
1133    // response in this case
1134    response.setContentLength(0);
1135 }
1166 if (!entityBody) {
1167    response.setContentLength(-1);
1168 }
Explicitly setting contentLength(0) in 1134 overrides by 1167, so response doesn't contain Content-Length header.
Comment 4 Alexandr Saperov 2017-07-26 09:50:12 UTC
Created attachment 35175 [details]
Patch 0-length content for 205 status
Comment 5 Violeta Georgieva 2017-07-26 09:52:06 UTC

(In reply to Alexandr Saperov from comment #4)
> Created attachment 35175 [details]
> Patch 0-length content for 205 status

Would you mind to add a test case also?

Comment 6 Alexandr Saperov 2017-07-26 11:57:48 UTC
Created attachment 35179 [details]
standalone application with embedded tomcat

Building standalone application with embedded tomcat:
mvn clean package

running application:
java -jar target/tomcat-61086-1.0-SNAPSHOT.jar

Making request with curl:
curl -v "localhost:8080/"

curl hangs for 1 minute (default timeout)
Comment 7 Violeta Georgieva 2017-08-01 07:59:47 UTC

Thanks for the patch and the test - see r1803616.

This has been fixed in:
- 9.0.x for 9.0.0.M26 onwards
- 8.5.x for 8.5.20 onwards
- 8.0.x for 8.0.46 onwards
- 7.0.x for 7.0.80 onwards