fix(ipintelligence): apply configured timeout to MMDB downloads and expose IP intelligence timeout option

This commit is contained in:
2026-05-21 01:42:48 +00:00
parent 23df951023
commit 8a4f756fef
4 changed files with 56 additions and 9 deletions
+10 -1
View File
@@ -1,5 +1,14 @@
# Changelog # 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) ## 2026-04-30 - 4.7.1 - fix(build)
enforce stricter TypeScript checks and update build dependencies enforce stricter TypeScript checks and update build dependencies
@@ -303,4 +312,4 @@ New feature added.
## 2017-12-12 - 1.0.1 - initial ## 2017-12-12 - 1.0.1 - initial
Initial commit. Initial commit.
- Initial project setup. - Initial project setup.
+26
View File
@@ -99,6 +99,32 @@ tap.test('should use cache when cacheTtl is set', async () => {
await cached.stop(); 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<Response>((_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 () => { tap.test('should stop cleanly (tears down shared smartdns client)', async () => {
// If the Rust-backed smartdns bridge wasn't destroyed, this test process // If the Rust-backed smartdns bridge wasn't destroyed, this test process
// would hang at exit instead of completing. // would hang at exit instead of completing.
+14 -7
View File
@@ -522,14 +522,21 @@ export class IpIntelligence {
* Fetch a URL and return the response as a Buffer * Fetch a URL and return the response as a Buffer
*/ */
private async fetchBuffer(url: string): Promise<Buffer> { private async fetchBuffer(url: string): Promise<Buffer> {
const response = await fetch(url, { const controller = new AbortController();
headers: { 'User-Agent': '@push.rocks/smartnetwork' }, const timeoutId = setTimeout(() => controller.abort(), this.timeout);
}); try {
if (!response.ok) { const response = await fetch(url, {
throw new Error(`Failed to fetch ${url}: HTTP ${response.status}`); 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);
} }
/** /**
+6 -1
View File
@@ -16,6 +16,8 @@ type TSmartdnsClient = InstanceType<typeof plugins.smartdns.dnsClientMod.Smartdn
export interface SmartNetworkOptions { export interface SmartNetworkOptions {
/** Cache time-to-live in milliseconds for gateway and public IP lookups */ /** Cache time-to-live in milliseconds for gateway and public IP lookups */
cacheTtl?: number; cacheTtl?: number;
/** Timeout in milliseconds for IP intelligence RDAP/DNS/MMDB requests. Default: 5000 */
ipIntelligenceTimeout?: number;
} }
/** /**
@@ -437,7 +439,10 @@ export class SmartNetwork {
*/ */
public async getIpIntelligence(ip: string): Promise<IIpIntelligenceResult> { public async getIpIntelligence(ip: string): Promise<IIpIntelligenceResult> {
if (!this.ipIntelligence) { 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); const fetcher = () => this.ipIntelligence!.getIntelligence(ip);
if (this.options.cacheTtl && this.options.cacheTtl > 0) { if (this.options.cacheTtl && this.options.cacheTtl > 0) {