Browse Source

fix(api): harden CoinGecko HTTP checks and Redis eviction policy

Add response status checks to CoinGecko API calls.
Set Redis maxmemory-policy to allkeys-lru for cache eviction.
Improve AI service error handling.
pull/6458/head
Ryan Waits 4 weeks ago
parent
commit
70c43c5532
  1. 4
      apps/api/src/app/endpoints/ai/ai.service.ts
  2. 11
      apps/api/src/app/redis-cache/redis-cache.service.ts
  3. 48
      apps/api/src/services/data-provider/coingecko/coingecko.service.ts

4
apps/api/src/app/endpoints/ai/ai.service.ts

@ -9,7 +9,7 @@ import type { AiPromptMode } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common';
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
import { generateText } from 'ai';
import { generateText, type LanguageModel } from 'ai';
import type { ColumnDescriptor } from 'tablemark';
@Injectable()
@ -55,7 +55,7 @@ export class AiService {
return generateText({
prompt,
model: openRouterService.chat(openRouterModel)
model: openRouterService.chat(openRouterModel) as unknown as LanguageModel
});
}

11
apps/api/src/app/redis-cache/redis-cache.service.ts

@ -76,22 +76,17 @@ export class RedisCacheService {
public async isHealthy() {
const testKey = '__health_check__';
const testValue = Date.now().toString();
try {
await Promise.race([
(async () => {
await this.set(testKey, testValue, ms('1 second'));
const result = await this.get(testKey);
if (result !== testValue) {
throw new Error('Redis health check failed: value mismatch');
}
await this.set(testKey, 'ok', ms('30 seconds'));
await this.get(testKey);
})(),
new Promise((_, reject) =>
setTimeout(
() => reject(new Error('Redis health check failed: timeout')),
ms('2 seconds')
ms('5 seconds')
)
)
]);

48
apps/api/src/services/data-provider/coingecko/coingecko.service.ts

@ -65,12 +65,20 @@ export class CoinGeckoService implements DataProviderInterface {
};
try {
const { name } = await fetch(`${this.apiUrl}/coins/${symbol}`, {
const res = await fetch(`${this.apiUrl}/coins/${symbol}`, {
headers: this.headers,
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
}).then((res) => res.json());
});
if (!res.ok) {
throw new Error(
`CoinGecko API returned ${res.status} for asset profile ${symbol}`
);
}
const { name } = await res.json();
response.name = name;
} catch (error) {
@ -116,13 +124,21 @@ export class CoinGeckoService implements DataProviderInterface {
vs_currency: DEFAULT_CURRENCY.toLowerCase()
});
const { error, prices, status } = await fetch(
const res = await fetch(
`${this.apiUrl}/coins/${symbol}/market_chart/range?${queryParams.toString()}`,
{
headers: this.headers,
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
);
if (!res.ok) {
throw new Error(
`CoinGecko API returned ${res.status} for historical data ${symbol}`
);
}
const { error, prices, status } = await res.json();
if (error?.status) {
throw new Error(error.status.error_message);
@ -179,13 +195,21 @@ export class CoinGeckoService implements DataProviderInterface {
vs_currencies: DEFAULT_CURRENCY.toLowerCase()
});
const quotes = await fetch(
const res = await fetch(
`${this.apiUrl}/simple/price?${queryParams.toString()}`,
{
headers: this.headers,
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
);
if (!res.ok) {
throw new Error(
`CoinGecko API returned ${res.status} for quotes ${symbols.join(', ')}`
);
}
const quotes = await res.json();
for (const symbol in quotes) {
response[symbol] = {
@ -228,13 +252,21 @@ export class CoinGeckoService implements DataProviderInterface {
query
});
const { coins } = await fetch(
const res = await fetch(
`${this.apiUrl}/search?${queryParams.toString()}`,
{
headers: this.headers,
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
);
if (!res.ok) {
throw new Error(
`CoinGecko API returned ${res.status} for search "${query}"`
);
}
const { coins } = await res.json();
items = coins.map(({ id: symbol, name }) => {
return {

Loading…
Cancel
Save