fix(ipintelligence): apply configured timeout to MMDB downloads and expose IP intelligence timeout option
This commit is contained in:
+10
-1
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user