diff --git a/src/contentFetcher.ts b/src/contentFetcher.ts index 9c37d0dc..93334e38 100644 --- a/src/contentFetcher.ts +++ b/src/contentFetcher.ts @@ -128,12 +128,15 @@ export class ContentFetcher { throw new Error('The API key must be provided to fetch static content.'); } - return new Promise((resolve, reject) => { + // Types for Browser and Node environment + let timeoutReference: number | NodeJS.Timeout | undefined; + + return new Promise>((resolve, reject) => { const abortController = new AbortController(); const timeout = options.timeout ?? this.configuration.defaultTimeout; if (timeout !== undefined) { - setTimeout( + timeoutReference = setTimeout( () => { const response: ErrorResponse = { title: `Content could not be loaded in time for slot '${slotId}'.`, @@ -189,7 +192,12 @@ export class ContentFetcher { ); } }); - }); + }) + .finally(() => { + // Once the fetch completes, regardless of outcome, cancel the timeout + // to avoid logging an error that didn't happen. + clearTimeout(timeoutReference); + }); } private load(slotId: string, signal: AbortSignal, options: FetchOptions): Promise { diff --git a/test/contentFetcher.test.ts b/test/contentFetcher.test.ts index 254d293b..fc90def0 100644 --- a/test/contentFetcher.test.ts +++ b/test/contentFetcher.test.ts @@ -414,6 +414,36 @@ describe('A content fetcher', () => { }); }); + it('should not log a timeout error message when request completes before the timeout', async () => { + jest.useFakeTimers(); + + const logger: Logger = { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + + const fetcher = new ContentFetcher({ + appId: appId, + logger: logger, + defaultTimeout: 10, + }); + + fetchMock.mock({ + ...requestMatcher, + response: { + result: 'Carol', + }, + }); + + await fetcher.fetch(slotId); + + jest.advanceTimersByTime(11); + + expect(logger.error).not.toHaveBeenCalled(); + }); + it('should fetch dynamic content using the provided context', async () => { const fetcher = new ContentFetcher({ appId: appId,