Volodymyr Panivko created THRIFT-6060:
-----------------------------------------

             Summary: C++ THttpClient does not reopen socket after server sends 
Connection: close
                 Key: THRIFT-6060
                 URL: https://issues.apache.org/jira/browse/THRIFT-6060
             Project: Thrift
          Issue Type: Bug
          Components: C++ - Library
            Reporter: Volodymyr Panivko


*Component:* C++ - Library / `lib/cpp/src/thrift/transport/THttpClient.{h,cpp}`

h3. Symptom

When the HTTP server emits `Connection: close` (RFC-correct for HTTP/1.1 when 
the server intends to close the connection after the response) and closes the 
TCP connection, the next call on the same {{THttpClient}} fails with one of:

* {{TSocket::write_partial() send(): Broken pipe}} on the write side, or
* {{Could not refill buffer}} (TTransportException::END_OF_FILE) on the 
subsequent read.

This happens deterministically on the *second* RPC over a single 
{{THttpClient}} instance.

h3. Root cause

{{THttpClient::flush()}} only reopens the underlying transport when it is not 
already open:
{code:cpp}
void THttpClient::flush() {
  ...
  if (!transport_->isOpen()) {
    transport_->open();
  }
  ...
}
{code}
After the first response is read, the wrapped {{TSocket}} still reports 
{{isOpen()==true}} (no read has yet detected EOF in some paths). The server has 
already sent `Connection: close` and torn down the TCP connection. The next 
{{flush()}} writes the new request to the half-closed socket, the kernel 
returns EPIPE, and any subsequent read returns 0 → `Could not refill buffer`.

Unlike the {{onewayResponsePending_}} fix landed in THRIFT-6021, this code path 
is not gated on oneway; it applies to *every* call after the first when the 
peer uses `Connection: close`.

h3. Reproduction (verified locally and in CI)

# Run any HTTP server that sets `Connection: close` after each response. PHP's 
built-in web server (`php -S host:port router.php`) is the simplest — its 
cli-server SAPI hardcodes `Connection: close`.
# Build the cross-test C++ TestClient.
# Invoke {{TestClient --protocol=binary --transport=http --port=N}}.
# Observe: `testVoid() = void`, then `testString("Test")terminate called ... 
what(): Could not refill buffer` (or `Broken pipe`).

A byte-perfect Python mimic of the cli-server response (HTTP/1.1 200, 
`Connection: close`, `Content-Length: N`, 21-byte body sent as a second TCP 
segment) reproduces the same failure pattern.

This is the underlying reason the {{php-cpp_*_http-ip}} cross-test cells stay 
in {{known_failures_Linux.json}} after THRIFT-6023 (PHP HTTP cross-test 
support) landed.

h3. Fix direction

In {{THttpClient::parseHeader}}, parse the {{Connection}} response header and 
set an internal `closeAfterResponse_` flag when the value is `close`. In 
{{flush()}}, after consuming the response, close the underlying transport if 
that flag is set, so the next {{flush()}} reopens it. Alternative: always 
reopen the transport for each request and rely on the underlying TSocket to 
keep TCP-level reuse out of scope.

The sibling Java/Python/PHP HTTP clients already reconnect per request (PHP's 
PSR-18 path is the {{TPsrHttpClient}} added in THRIFT-6010), which is why they 
cross-test cleanly against the same PHP server.

h3. Related

* THRIFT-6021 / [#3514|https://github.com/apache/thrift/pull/3514] — addressed 
the oneway-response variant of the same connection-state problem.
* THRIFT-6023 / [#3515|https://github.com/apache/thrift/pull/3515] — adds PHP 
HTTP cross-tests; the four {{php-cpp_*_http-ip}} known_failures entries in that 
PR are blocked on this ticket.



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

Reply via email to