[
https://issues.apache.org/jira/browse/THRIFT-6060?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Jens Geyer reassigned THRIFT-6060:
----------------------------------
Assignee: Stefan Wang
> 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
> Affects Versions: 0.23.0
> Reporter: Volodymyr Panivko
> Assignee: Stefan Wang
> Priority: Major
> Fix For: 0.25.0
>
> Time Spent: 40m
> Remaining Estimate: 0h
>
> *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)