feat: add 5s timeout and warning log for station name search (refs #141)

Add 5000ms timeout to Places Text Search API call in searchStationByName.
Timeout errors log a warning instead of error and return null gracefully.
Add timeout test case to station-matching unit tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eric Gullickson
2026-02-11 13:03:35 -06:00
parent c79b610145
commit 4e5da4782f
2 changed files with 25 additions and 2 deletions

View File

@@ -128,6 +128,7 @@ export class GoogleMapsClient {
type: 'gas_station', type: 'gas_station',
key: this.apiKey, key: this.apiKey,
}, },
timeout: 5000,
} }
); );
@@ -145,8 +146,12 @@ export class GoogleMapsClient {
await cacheService.set(cacheKey, station, this.cacheTTL); await cacheService.set(cacheKey, station, this.cacheTTL);
return station; return station;
} catch (error) { } catch (error: any) {
logger.error('Station name search failed', { error, merchantName }); if (error.code === 'ECONNABORTED' || error.message?.includes('timeout')) {
logger.warn('Station name search timed out', { merchantName, timeoutMs: 5000 });
} else {
logger.error('Station name search failed', { error, merchantName });
}
return null; return null;
} }
} }

View File

@@ -32,6 +32,7 @@ import { GoogleMapsClient } from '../../external/google-maps/google-maps.client'
import { StationsService } from '../../domain/stations.service'; import { StationsService } from '../../domain/stations.service';
import { StationsRepository } from '../../data/stations.repository'; import { StationsRepository } from '../../data/stations.repository';
import { googleMapsClient } from '../../external/google-maps/google-maps.client'; import { googleMapsClient } from '../../external/google-maps/google-maps.client';
import { logger } from '../../../../core/logging/logger';
import { mockStations } from '../fixtures/mock-stations'; import { mockStations } from '../fixtures/mock-stations';
describe('Station Matching from Receipt', () => { describe('Station Matching from Receipt', () => {
@@ -162,6 +163,23 @@ describe('Station Matching from Receipt', () => {
expect(result).toBeNull(); expect(result).toBeNull();
}); });
it('should return null with logged warning on Places API timeout', async () => {
const timeoutError = new Error('timeout of 5000ms exceeded') as any;
timeoutError.code = 'ECONNABORTED';
mockAxios.get.mockRejectedValue(timeoutError);
const mockLogger = logger as jest.Mocked<typeof logger>;
const result = await client.searchStationByName('Shell');
expect(result).toBeNull();
expect(mockLogger.warn).toHaveBeenCalledWith(
'Station name search timed out',
expect.objectContaining({ merchantName: 'Shell', timeoutMs: 5000 })
);
expect(mockLogger.error).not.toHaveBeenCalled();
});
it('should include rating and photo reference when available', async () => { it('should include rating and photo reference when available', async () => {
mockAxios.get.mockResolvedValue({ mockAxios.get.mockResolvedValue({
data: { data: {