Repository navigation
[CHA-5724] fix: retry requests on dropped connections - #178
Closed
nijeesh-stream wants to merge 2 commits into
Closed
nijeesh-stream wants to merge 2 commits into
nijeesh-stream wants to merge 2 commits into
Conversation
Load balancers in front of the API occasionally drop a connection, which surfaces as cURL error 56 "unexpected eof while reading". The default HTTP client now retries up to 2 times with a short jittered backoff: any method when the connection was never established (cURL 6, 7, 35), and only GET, HEAD and OPTIONS when the connection dropped after the request may have been sent (cURL 52, 55, 56). HTTP responses and timeouts are never retried. Clients set with setHttpClient are unchanged; Client::retryMiddleware() exposes the same policy for their handler stack. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nijeesh-stream
requested review from
JimmyPettersson85 and
yaziine
as code owners
October 20, 2026 12:43
curl reports a connection closed after the request was sent as RECV_ERROR (56) or GOT_NOTHING (52) depending on the curl/OpenSSL build; the Ubuntu CI runner reports 52. The retry policy already treats both alike, so assert either. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
Closing without merging: CHA-5724 has been closed (no further customer reports). Keeping this PR and branch as a reference implementation in case the issue comes back. Context for whoever picks it up:
Findings are documented on https://linear.app/stream/issue/CHA-5724 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TLDR
cURL error 56: OpenSSL SSL_read: unexpected eof while reading. Google says clients have to cope with dropped connections.GET /unread.GET,HEADandOPTIONSare retried. The rule looks only at the curl error and the HTTP method, never at the endpoint.setHttpClient()are left as they are.Client::retryMiddleware()lets those users opt in.Ticket
Summary
Client::__constructpushesClient::retryMiddleware()(built onGuzzleHttp\Middleware::retry) onto theHandlerStackit creates. The decision uses the curl errno fromgetHandlerContext()['errno'], not the exception class. Guzzle maps errors to exception classes inconsistently: 28, 6, 7, 35 and 52 becomeConnectException, everything else becomesRequestException.COULDNT_RESOLVE_HOST, 7COULDNT_CONNECT, 35SSL_CONNECT_ERRORGOT_NOTHING, 55SEND_ERROR, 56RECV_ERRORGET,HEAD,OPTIONSPOST,PUT,PATCH,DELETEBackoff: 50–100ms before the 1st retry and 100–200ms before the 2nd, so at most 300ms in total.
Public API added:
Client::retryMiddleware(): callable, documented in the README FAQ for users who inject their own Guzzle client. Everything else is private.Evidence
Local reproductions (TLS server written in PHP, curl with OpenSSL 3, Guzzle 7.16):
close_notify, the client getsRequestExceptionwith errno 56. The message is identical to the customer's:cURL error 56: OpenSSL SSL_read: OpenSSL/3.5.4: error:0A000126:SSL routines::unexpected eof while reading. The server did receive the request.RequestException), like the customer. The Ubuntu GitHub runner reports 52Empty reply from server(ConnectException). The policy treats 52 and 56 the same way (retry only GET/HEAD/OPTIONS), so both are covered. The real-socket tests accept either errno and assert the attempt counts exactly.ConnectExceptionwith errno 35, and the request was never sent.Tests added in
tests/unit/RetryTest.php. The first 5 use a real TLS server (tests/unit/fixtures/tls_server.php) started as a subprocess, and go through the SDK's default client. The errno of each attempt is recorded withon_stats:unreadCounts) gets a mid-flight drop, then a 200 → succeeds. The server saw 2 GETs; the first attempt's errno was 52 or 56, the second 0.upsertUser) gets a mid-flight drop → throws errno 52 or 56. The server saw exactly 1 POST, so there was no retry.[35, 0]. The JSON body arrived intact on the successful attempt, which shows the body is rewound.ServerException. The server saw 1 request.First run on the committed branch (
f9e84c9) with PHP 8.1, Guzzle 7.16.0 and the repo'sdocker/Dockerfile:After the follow-up
test:commit (961eb0b, which accepts errno 52 or 56), the same Docker run gave: unit suiteOK (57 tests, 144 assertions),RetryTestOK (9 tests, 78 assertions), and php-cs-fixer and phan both exit 0.Other runs:
php:8.3-cli-alpine(curl 8.22, OpenSSL 3.5.8, close to the customer's setup), the full unit suite passed:OK (57 tests, 138 assertions).RetryTestpassed:OK (9 tests, 72 assertions). That confirmserrnois in the handler context and the body is rewound on Guzzle 6 as well.Risks
setHttpClient(), your client is used exactly as you configured it. You can opt in withClient::retryMiddleware().ext-curlis missing, Guzzle's stream handler gives no errno, so no retries happen. Behaviour stays as it is today.🤖 Generated with Claude Code