diff --git a/changelog.md b/changelog.md index 52dc39e..1a7b8ee 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## Pending + +### Fixes + +- apply configured timeout to MMDB downloads and expose IP intelligence timeout option (ipintelligence) + - wrap MMDB fetch requests with AbortController-based timeout handling + - pass SmartNetwork ipIntelligenceTimeout through to IpIntelligence initialization + - add test coverage to verify timed-out MMDB downloads are aborted + ## 2026-04-30 - 4.7.1 - fix(build) enforce stricter TypeScript checks and update build dependencies @@ -303,4 +312,4 @@ New feature added. ## 2017-12-12 - 1.0.1 - initial Initial commit. -- Initial project setup. \ No newline at end of file +- Initial project setup. diff --git a/test/test.ipintelligence.ts b/test/test.ipintelligence.ts index d8e73ce..e083ff6 100644 --- a/test/test.ipintelligence.ts +++ b/test/test.ipintelligence.ts @@ -99,6 +99,32 @@ tap.test('should use cache when cacheTtl is set', async () => { await cached.stop(); }); +tap.test('should apply timeout to IP intelligence MMDB downloads', async () => { + const originalFetch = globalThis.fetch; + let sawAbort = false; + globalThis.fetch = (async (_url: RequestInfo | URL, init?: RequestInit) => { + return await new Promise((_resolve, reject) => { + init?.signal?.addEventListener('abort', () => { + sawAbort = true; + reject(new Error('aborted')); + }, { once: true }); + }); + }) as typeof fetch; + + const intelligence = new smartnetwork.IpIntelligence({ timeout: 10 }); + let caught: Error | undefined; + try { + await (intelligence as any).fetchBuffer('https://example.com/test.mmdb'); + } catch (error) { + caught = error as Error; + } finally { + globalThis.fetch = originalFetch; + } + + expect(sawAbort).toEqual(true); + expect(caught).toBeTruthy(); +}); + tap.test('should stop cleanly (tears down shared smartdns client)', async () => { // If the Rust-backed smartdns bridge wasn't destroyed, this test process // would hang at exit instead of completing. diff --git a/ts/smartnetwork.classes.ipintelligence.ts b/ts/smartnetwork.classes.ipintelligence.ts index 2f15d92..e7c3519 100644 --- a/ts/smartnetwork.classes.ipintelligence.ts +++ b/ts/smartnetwork.classes.ipintelligence.ts @@ -522,14 +522,21 @@ export class IpIntelligence { * Fetch a URL and return the response as a Buffer */ private async fetchBuffer(url: string): Promise { - const response = await fetch(url, { - headers: { 'User-Agent': '@push.rocks/smartnetwork' }, - }); - if (!response.ok) { - throw new Error(`Failed to fetch ${url}: HTTP ${response.status}`); + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), this.timeout); + try { + const response = await fetch(url, { + signal: controller.signal, + headers: { 'User-Agent': '@push.rocks/smartnetwork' }, + }); + if (!response.ok) { + throw new Error(`Failed to fetch ${url}: HTTP ${response.status}`); + } + const arrayBuffer = await response.arrayBuffer(); + return Buffer.from(arrayBuffer); + } finally { + clearTimeout(timeoutId); } - const arrayBuffer = await response.arrayBuffer(); - return Buffer.from(arrayBuffer); } /** diff --git a/ts/smartnetwork.classes.smartnetwork.ts b/ts/smartnetwork.classes.smartnetwork.ts index e0ea951..37047ef 100644 --- a/ts/smartnetwork.classes.smartnetwork.ts +++ b/ts/smartnetwork.classes.smartnetwork.ts @@ -16,6 +16,8 @@ type TSmartdnsClient = InstanceType { if (!this.ipIntelligence) { - this.ipIntelligence = new IpIntelligence({ dnsClient: this.ensureDnsClient() }); + this.ipIntelligence = new IpIntelligence({ + dnsClient: this.ensureDnsClient(), + timeout: this.options.ipIntelligenceTimeout, + }); } const fetcher = () => this.ipIntelligence!.getIntelligence(ip); if (this.options.cacheTtl && this.options.cacheTtl > 0) {