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)