Daniel Sun created GROOVY-12287:
-----------------------------------

             Summary: HttpBuilder: retry once on a stale keep-alive connection
                 Key: GROOVY-12287
                 URL: https://issues.apache.org/jira/browse/GROOVY-12287
             Project: Groovy
          Issue Type: Bug
            Reporter: Daniel Sun


HttpBuilder follows redirects itself whenever {{followRedirects}} is enabled. 
The next hop goes through the same JDK {{HttpClient}} and may reuse a 
keep-alive connection the peer has already closed.

JDK {{HttpClient}} retries that failure for GET/HEAD, but not for 
PUT/POST/PATCH. A same-origin 301 of a PUT then fails intermittently:

{noformat}
java.lang.RuntimeException: I/O error during HTTP request PUT 
http://127.0.0.1:.../api/put-dst
Caused by: java.io.IOException: HTTP/1.1 header parser received no bytes
Caused by: java.io.IOException: Broken pipe
{noformat}

Seen on 
{{HttpBuilderTest.confineToBaseUriPreservesNonPostMethodAcrossRedirectOn301}} 
(GROOVY-12182). Redirect following is always manual (GROOVY-12274). GET 
follow-ups do not flake: the JDK client retries them.

h2. Cause

A common trigger is {{com.sun.net.httpserver.HttpServer}}: 
{{sendResponseHeaders(status, -1)}} closes the exchange immediately. If the 
request body was never read, that close drops the TCP connection *without* 
sending {{Connection: close}}. The client pools the socket, reuses it for the 
next hop, and PUT is not retried.

The same class of failure happens against any HTTP/1.1 peer that silently drops 
a keep-alive connection (idle timeout, 3xx of a request with a body, etc.).

h2. Expected

With {{followRedirects true}}, a PUT that receives 301 should reach the 
{{Location}} target with method and body preserved 
({{HttpClient.Redirect.NORMAL}}).

h2. Actual

The follow-up PUT occasionally throws the I/O error above before any response 
bytes are read.

h2. Fix

# Retry *once* in {{HttpBuilder.send}} / {{sendAsync}} when the I/O error 
indicates a stale connection ({{header parser received no bytes}}, broken pipe, 
connection reset). Do not retry timeouts or interrupts.
# Drain the request body in test {{HttpServer}} redirect handlers *before* 
{{sendResponseHeaders(status, -1)}}, so the mock server does not drop the 
socket without advertising it.

Regression: {{HttpBuilderStaleConnectionTest}} (peer consumes the request and 
closes with no response; PUT and {{putAsync}} recover on the second connection).




--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to