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)