Haifeng Hu created HTTPCORE-796:
-----------------------------------
Summary: HttpRequestExecutor sends request body on 307 redirect
with Expect: 100-continue, causing Broken Pipe for payloads >100KB
Key: HTTPCORE-796
URL: https://issues.apache.org/jira/browse/HTTPCORE-796
Project: HttpComponents HttpCore
Issue Type: Improvement
Components: HttpCore
Affects Versions: 5.5-alpha1, 5.4.2, 5.3.1, 5.2.4
Reporter: Haifeng Hu
Issue Title
HttpRequestExecutor sends request body on 307 redirect with Expect:
100-continue, causing Broken Pipe for payloads >100KB
h2. Project
HttpComponents HttpCore (HTTPCORE)
h2. Issue Type
Bug
h2. Affects Versions
5.2.4, 5.3.1, 5.4.2, 5.5-beta1 (all versions from 5.2.x through latest master)
h2. Description
h3. Summary
HttpRequestExecutor.execute() in httpcore5 incorrectly sends the request body
when it receives a *3xx redirect response* (e.g., 307 Temporary Redirect) while
waiting for a 100-continue interim response. This causes a *Broken Pipe* error
for payloads exceeding the TCP send buffer (~100KB / 70 MSS), because the
server has already closed the connection.
This bug is present in *all versions from 5.2.x through 5.5-beta1* (latest
master as of 2026-06-17).
h3. Affected File
httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/HttpRequestExecutor.java
h3. Root Cause
In the execute() method, when Expect: 100-continue is set and the server
responds with a non-1xx status code, the code branches as follows:
if (status == HttpStatus.SC_CONTINUE) {
// discard 100-continue
response = null;
conn.sendRequestEntity(request); // 100 -> send body (correct)
} else if (status < HttpStatus.SC_SUCCESS) {
// other 1xx -> continue waiting
if (informationCallback != null) {
informationCallback.execute(response, conn, context);
}
response = null;
continue;
} else if (status >= HttpStatus.SC_CLIENT_ERROR) {
conn.terminateRequest(request); // 4xx/5xx -> terminate (correct)
} else {
conn.sendRequestEntity(request); // BUG: 2xx/3xx (including 307)
-> sends body!
}
The else branch treats 2xx and 3xx identically: it sends the request body. For
2xx this is correct (the server accepted the request). But for 3xx redirects
(especially 307), the server is saying "I won't accept this body, go elsewhere"
-- yet httpcore5 sends the body anyway.
h3. Impact
When a server (e.g., StarRocks FE, or any load-balancer/front-end returning 307
with Expect: 100-continue) closes the connection after sending the redirect:
# *Payload < ~100KB (fits in TCP send buffer):* sendRequestEntity() writes the
entire body into the local TCP buffer without blocking, returns normally. The
307 response is passed up to RedirectExec, which correctly redirects to the new
location. *Appears to work* (but wastes bandwidth sending body to the wrong
server).
# *Payload > ~100KB (exceeds TCP send buffer):* sendRequestEntity() blocks
waiting for buffer space. The server has already sent FIN, then RST (because it
received unexpected data on a closing connection). write() throws
SocketException: Broken pipe. The exception propagates up through
HttpRequestRetryExec, which retries the request to the *original URL* (not the
redirect target), resulting in a second failure.
h3.
h3. Comparison with HttpClient 4.x (Correct Behavior)
In httpcore 4.x, the equivalent code correctly handles this case:
// httpcore 4.x HttpRequestExecutor.java
if (conn.isResponseAvailable(tms)) {
response = conn.receiveResponseHeader();
int status = response.getStatusLine().getStatusCode();
if (status < 200) {
if (status != HttpStatus.SC_CONTINUE) {
throw new ProtocolException("Unexpected response: " +
response.getStatusLine());
}
response = null; // discard 100-continue
} else {
sendentity = false; // ALL responses >= 200 -> do NOT send body
(correct)
}
}
HttpClient 4.x sets sendentity = false for *all* responses with status >= 200,
which correctly prevents sending the body on 307 redirects.
h3.
h3. Suggested Fix
Replace the else branch with conn.terminateRequest(request) for all non-100
responses >= 200:
if (status == HttpStatus.SC_CONTINUE) {
response = null;
conn.sendRequestEntity(request);
} else if (status < HttpStatus.SC_SUCCESS) {
if (informationCallback != null) {
informationCallback.execute(response, conn, context);
}
response = null;
continue;
} else {
// FIX: For ALL responses >= 200 (2xx, 3xx, 4xx, 5xx),
// do NOT send the request body. The server has already responded
// and does not expect the body.
conn.terminateRequest(request);
}
This aligns with: *
HttpClient 4.x behavior (sendentity = false for all status >= 200)
*
RFC 7231 Section 5.1.1: a server responding to Expect: 100-continue with a
final status code indicates it does not need to receive the request body
*
h3. Reproduction
The bug was discovered with *Apache HttpClient 5.3.1 + httpcore5 5.2.4* during
StarRocks Stream Load operations, where the FE (Frontend) node returns 307
Temporary Redirect to a BE (Backend) node. The issue is reproducible with:
*
Single-threaded PUT request with Expect: 100-continue header
*
Payload > 101,360 bytes (70 x 1448 MSS, exceeding the default TCP send buffer)
*
Server returns 307 + FIN immediately after sending headers
*
Result: java.net.SocketException: Broken pipe
The same code works correctly with HttpClient 4.x.
h3. Environment
*
httpcore5 versions tested: 5.2.4, 5.4.2, 5.5-beta1 (master)
*
httpclient5 version: 5.3.1
*
JDK: 17+
*
OS: Linux (TCP sndbuf ~87KB via kernel auto-tuning)
*
h3. Workaround
Users can subclass HttpRequestExecutor and override the execute() method to
call conn.terminateRequest(request) for all non-100 responses >= 200, then
inject the custom executor into their HttpClient configuration.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]