fix(currency): degrade gracefully when rates return 404 (#449)
## Problem `PHP-LARAVEL-2M` (High, escalating, 38 events) — `RuntimeException: Failed to fetch currency rates for xxx on 2025-12-31: HTTP 404` crashing `/dashboard`. A user holds an account in currency `xxx` (ISO 4217 "no/unknown currency"). No rate file exists for it, so every candidate URL returns 404. `CurrencyConversionService::fetchRates` threw an unhandled `RuntimeException` that bubbled past the graceful-degradation logic in `ExchangeRateService::convert` and broke the whole dashboard render. ## Fix Distinguish **permanent** 404s from **transient** failures: - All sources return 404 → log a warning and return `[]`. Callers already handle empty rates (return unconverted / zero amount). - Timeouts / 5xx / connection errors → still throw, so real outages stay visible. ## Tests - New: unknown base currency with all-404 sources returns `0.0` instead of throwing. - Existing "throws when both primary and fallback fail" (500s) still passes — transient errors still surface. Fixes PHP-LARAVEL-2M.
This commit is contained in:
parent
5119528149
commit
bef657c2ed
|
|
@ -86,6 +86,11 @@ class CurrencyConversionService
|
||||||
foreach ($this->rateUrls($currency, $candidateDate) as $url) {
|
foreach ($this->rateUrls($currency, $candidateDate) as $url) {
|
||||||
try {
|
try {
|
||||||
$response = Http::timeout(10)->get($url);
|
$response = Http::timeout(10)->get($url);
|
||||||
|
|
||||||
|
if ($response->notFound()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$response->throw();
|
$response->throw();
|
||||||
|
|
||||||
return $response->json($currency) ?? [];
|
return $response->json($currency) ?? [];
|
||||||
|
|
@ -95,7 +100,16 @@ class CurrencyConversionService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new RuntimeException("Failed to fetch currency rates for {$currency} on {$date}: {$lastException?->getMessage()}", 0, $lastException);
|
if ($lastException !== null) {
|
||||||
|
throw new RuntimeException("Failed to fetch currency rates for {$currency} on {$date}: {$lastException->getMessage()}", 0, $lastException);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::warning('Currency rates unavailable, all sources returned 404', [
|
||||||
|
'currency' => $currency,
|
||||||
|
'date' => $date,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,18 @@ test('throws when both primary and fallback fail', function () {
|
||||||
$service->convert('BTC', 'EUR', 1.0, '2026-01-15');
|
$service->convert('BTC', 'EUR', 1.0, '2026-01-15');
|
||||||
})->throws(RuntimeException::class);
|
})->throws(RuntimeException::class);
|
||||||
|
|
||||||
|
test('returns zero when base currency is unknown and all sources return 404', function () {
|
||||||
|
Http::fake([
|
||||||
|
'cdn.jsdelivr.net/*' => Http::response('Not Found', 404),
|
||||||
|
'currency-api.pages.dev/*' => Http::response('Not Found', 404),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$service = new CurrencyConversionService;
|
||||||
|
$result = $service->convert('BTC', 'XXX', 1.0, '2026-01-15');
|
||||||
|
|
||||||
|
expect($result)->toBe(0.0);
|
||||||
|
});
|
||||||
|
|
||||||
test('falls back to previous historical date when requested release is missing', function () {
|
test('falls back to previous historical date when requested release is missing', function () {
|
||||||
Http::fake([
|
Http::fake([
|
||||||
'cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@2025-12-10/*' => Http::response('Not Found', 404),
|
'cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@2025-12-10/*' => Http::response('Not Found', 404),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue