fix(rates): reduce cache TTL, expose fallback header and show stale data warning

This commit is contained in:
2026-03-09 18:17:01 +01:00
parent 1f86a5ead4
commit 10493826bf
4 changed files with 31 additions and 6 deletions

View File

@@ -5,7 +5,7 @@ import {
RATES_CACHE_CONTROL_VALUE, RATES_CACHE_CONTROL_VALUE,
} from "@/lib/server/rates-cache"; } from "@/lib/server/rates-cache";
export const revalidate = 300; export const revalidate = 60;
export async function GET() { export async function GET() {
try { try {
@@ -19,13 +19,18 @@ export async function GET() {
}); });
} catch (error) { } catch (error) {
const cachedRates = getLastCachedRates(); const cachedRates = getLastCachedRates();
const fallbackErrorMessage =
error instanceof Error
? error.message.replace(/[\r\n]+/g, " ")
: "Upstream provider error";
if (cachedRates) { if (cachedRates) {
return NextResponse.json(cachedRates, { return NextResponse.json(cachedRates, {
status: 200, status: 200,
headers: { headers: {
"Cache-Control": RATES_CACHE_CONTROL_VALUE, "Cache-Control": RATES_CACHE_CONTROL_VALUE,
"X-Cache-Fallback": "stale-on-error" "X-Cache-Fallback": "stale-on-error",
"X-Cache-Fallback-Error": fallbackErrorMessage
} }
}); });
} }

View File

@@ -243,6 +243,24 @@ export function ConverterCard({
} }
}; };
const displayUpdatedAt = useMemo(() => {
if (!data) {
return null;
}
const timestamps = [new Date(data.updatedAt).getTime()];
if (marketData?.updatedAt) {
timestamps.push(new Date(marketData.updatedAt).getTime());
}
const latest = Math.max(
...timestamps.filter((timestamp) => Number.isFinite(timestamp)),
);
return Number.isFinite(latest) ? new Date(latest).toISOString() : data.updatedAt;
}, [data, marketData?.updatedAt]);
if (isLoading && !data) { if (isLoading && !data) {
return <ConverterSkeleton />; return <ConverterSkeleton />;
} }
@@ -497,7 +515,7 @@ export function ConverterCard({
<div> <div>
<p className="text-xs uppercase tracking-[0.12em]">Last updated</p> <p className="text-xs uppercase tracking-[0.12em]">Last updated</p>
<p className="mt-1 text-sm text-foreground"> <p className="mt-1 text-sm text-foreground">
{formatTimestamp(data.updatedAt)} {formatTimestamp(displayUpdatedAt ?? data.updatedAt)}
</p> </p>
</div> </div>
</div> </div>

View File

@@ -28,9 +28,11 @@ export function useMarketRates() {
const payload = await response.json(); const payload = await response.json();
const parsed = parseRatesResponse(payload); const parsed = parseRatesResponse(payload);
const isFallback = response.headers.get("X-Cache-Fallback") === "stale-on-error";
const fallbackError = response.headers.get("X-Cache-Fallback-Error");
setData(parsed); setData(parsed);
setError(null); setError(isFallback ? (fallbackError ?? "Upstream provider error") : null);
} catch (err) { } catch (err) {
const message = err instanceof Error ? err.message : "Unknown error"; const message = err instanceof Error ? err.message : "Unknown error";
setError(message); setError(message);

View File

@@ -1,9 +1,9 @@
import { fetchUnifiedRates } from "@/lib/api/normalize"; import { fetchUnifiedRates } from "@/lib/api/normalize";
import type { RatesResponse } from "@/lib/rates"; import type { RatesResponse } from "@/lib/rates";
export const RATES_CACHE_TTL_MS = 300_000; export const RATES_CACHE_TTL_MS = 60_000;
export const RATES_CACHE_CONTROL_VALUE = export const RATES_CACHE_CONTROL_VALUE =
"s-maxage=300, stale-while-revalidate=1800"; "s-maxage=60, stale-while-revalidate=600";
let cachedRates: RatesResponse | null = null; let cachedRates: RatesResponse | null = null;
let cacheTimestamp = 0; let cacheTimestamp = 0;