[
https://issues.apache.org/jira/browse/HTTPCORE-796?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18090142#comment-18090142
]
Oleg Kalnichevski edited comment on HTTPCORE-796 at 6/19/26 11:14 AM:
----------------------------------------------------------------------
[~rschmitt] Unlike HTTP/2 where the server endpoint can safely terminate the
incoming message stream by sending back a stream reset packet, HTTP/1.1 has no
mechanism of early message termination in case the message body is no longer
wanted or needed. However, it still does have a hard requirement of message
frame completeness. That makes it impossible to cleanly terminate a
Content-Length delimited message prematurely. With the chunk encoding the
server may still expect to get message trailers even if it intends to terminate
the underlying connection afterwards.
As far as I am concerned presence of `Connection: close` in a final response
does not grant the client an absolution to corrupt its request message frame.
At the same time I see no point in being a greater Catholic than the Pope of
Rome. In practical terms, what is it exactly you are proposing that we do?
Treat 3xx early responses as errors as we did in HC 4? Do so, if if we get
`Connection: close` in the redirect response? Something else?
Oleg
was (Author: olegk):
[~rschmitt] Unlike HTTP/2 where the server endpoint can safely terminate the
incoming message stream by sending back a stream reset packet, HTTP/1.1 has no
mechanism of early message termination in case the message body is no longer
wanted or needed. However, it still does have a hard requirement of message
frame completeness. That makes it impossible to cleanly terminate a
Content-Length delimited message prematurely. With the chunk encoding the
server may still expect to get message trailers even if it intends to terminate
the underlying connection afterwards.
As far as I am concerned presence of `Connection: close` in a final response
does not grant the client an absolution to corrupt its request message frame.
At the same time I see no point in being a greater Catholic than the Pope of
Rome. In practical terms what is. it exactly you are proposing that we do?
Treat 3xx early responses as errors as we did in HC 4? Do so if if we get
`Connection: close` in the redirect response? Something else?
Oleg
> 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.2.4, 5.3.1, 5.4.2, 5.5-alpha1
> Reporter: Haifeng Hu
> Priority: Critical
>
> 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]