fix(banking): treat 429 rate limit as transient, skip error status on sync (#224)

## Summary

- A `429 ASPSP_RATE_LIMIT_EXCEEDED` response from the bank's API was
incorrectly marking connections as `status=error`, blocking all future
syncs.
- Rate limit errors are transient — the connection is still valid and
should be retried on the next scheduled sync.
- Added `isRateLimitError()` check in the `catch` block of
`SyncBankingConnectionJob`: on 429, the job returns early without
updating the connection status or error message.
This commit is contained in:
Víctor Falcón 2026-03-16 10:59:46 +00:00 committed by GitHub
parent d5735b59c7
commit 5b9ae2a525
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 40 additions and 0 deletions

View File

@ -80,6 +80,10 @@ class SyncBankingConnectionJob implements ShouldBeUnique, ShouldQueue
'error' => $e->getMessage(),
]);
if ($this->isRateLimitError($e)) {
return;
}
$connection->update([
'status' => BankingConnectionStatus::Error,
'error_message' => $this->friendlyErrorMessage($e),
@ -203,6 +207,11 @@ class SyncBankingConnectionJob implements ShouldBeUnique, ShouldQueue
return __('An unexpected error occurred during sync. Please try again later.');
}
private function isRateLimitError(\Throwable $e): bool
{
return $e instanceof RequestException && $e->response->status() === 429;
}
private function isAuthError(\Throwable $e): bool
{
return $e instanceof RequestException

View File

@ -828,3 +828,34 @@ test('sends auth failed email for binance 403 error on final attempt', function
&& $mail->bankingConnection->id === $connection->id;
});
});
test('rate limit error does not set connection status to error', function () {
$user = User::factory()->onboarded()->create();
$connection = BankingConnection::factory()->create([
'user_id' => $user->id,
'last_synced_at' => now()->subDay(),
]);
Account::factory()->connected()->create([
'user_id' => $user->id,
'banking_connection_id' => $connection->id,
'external_account_id' => 'ext-123',
]);
$transactionSync = Mockery::mock(TransactionSyncService::class);
$transactionSync->shouldReceive('sync')->andThrow(
new \Illuminate\Http\Client\RequestException(
new \Illuminate\Http\Client\Response(
new \GuzzleHttp\Psr7\Response(429)
)
)
);
$balanceSync = Mockery::mock(BalanceSyncService::class);
$job = new SyncBankingConnectionJob($connection);
$job->handle($transactionSync, $balanceSync);
$connection->refresh();
expect($connection->status)->toBe(BankingConnectionStatus::Active);
expect($connection->error_message)->toBeNull();
});