From 4e5da4782ff01242b1f4e28d00e653a1902a0d0e Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Wed, 11 Feb 2026 13:03:35 -0600 Subject: [PATCH] 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 --- .../external/google-maps/google-maps.client.ts | 9 +++++++-- .../tests/unit/station-matching.test.ts | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/backend/src/features/stations/external/google-maps/google-maps.client.ts b/backend/src/features/stations/external/google-maps/google-maps.client.ts index 4336065..00ac5ba 100644 --- a/backend/src/features/stations/external/google-maps/google-maps.client.ts +++ b/backend/src/features/stations/external/google-maps/google-maps.client.ts @@ -128,6 +128,7 @@ export class GoogleMapsClient { type: 'gas_station', key: this.apiKey, }, + timeout: 5000, } ); @@ -145,8 +146,12 @@ export class GoogleMapsClient { await cacheService.set(cacheKey, station, this.cacheTTL); return station; - } catch (error) { - logger.error('Station name search failed', { error, merchantName }); + } catch (error: any) { + 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; } } diff --git a/backend/src/features/stations/tests/unit/station-matching.test.ts b/backend/src/features/stations/tests/unit/station-matching.test.ts index 20d9569..e4aee10 100644 --- a/backend/src/features/stations/tests/unit/station-matching.test.ts +++ b/backend/src/features/stations/tests/unit/station-matching.test.ts @@ -32,6 +32,7 @@ import { GoogleMapsClient } from '../../external/google-maps/google-maps.client' import { StationsService } from '../../domain/stations.service'; import { StationsRepository } from '../../data/stations.repository'; import { googleMapsClient } from '../../external/google-maps/google-maps.client'; +import { logger } from '../../../../core/logging/logger'; import { mockStations } from '../fixtures/mock-stations'; describe('Station Matching from Receipt', () => { @@ -162,6 +163,23 @@ describe('Station Matching from Receipt', () => { 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; + + 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 () => { mockAxios.get.mockResolvedValue({ data: {