Skip to content

Commit 1c6ae49

Browse files
committed
Reuse HTTPS connections through an undici Agent and negotiate HTTP/2
Every lookup used to call the global fetch with its default dispatcher, so a client had no connection pool of its own to control or close. IPData now creates an undici Agent together with the instance (allowH2 for HTTP/2 via ALPN with HTTP/1.1 fallback, 60 s idle timeout, 30 s HTTP/2 PING and TCP keepalive probes) and passes it to every fetch call, so TCP and TLS are negotiated once per client instead of once per request. A trailing, optional httpConfig constructor argument exposes http2, keepAliveTimeout and dispatcher (to inject a ProxyAgent, EnvHttpProxyAgent or MockAgent). close() releases an Agent the instance created and leaves an injected dispatcher alone. A request that fails before any response byte arrived because a pooled connection was reset or closed is retried exactly once; HTTP statuses are never retried. The bulk POST now also sends Accept: application/json. Tests replace the global.fetch patch with an undici MockAgent injected through httpConfig.dispatcher, and a loopback HTTP server proves that consecutive requests reuse one connection and that close() releases it. CI runs on pull requests and on Node 20, 22 and 24. undici ^7.22.0 needs Node >= 20.18.1 (lru-cache 11 already excluded Node 18), the version becomes 3.1.0 and the lock file is regenerated. Claude-Session: https://claude.ai/code/session_01VQiRp4zEJKUXHrKkFaeUke Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent b87caed commit 1c6ae49

6 files changed

Lines changed: 471 additions & 52 deletions

File tree

‎.github/workflows/default.yml‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ on:
44
push:
55
branches:
66
- '**'
7+
pull_request:
78

89
jobs:
910
lint:
@@ -22,13 +23,16 @@ jobs:
2223

2324
test:
2425
runs-on: ubuntu-latest
26+
strategy:
27+
matrix:
28+
node-version: [20, 22, 24]
2529
steps:
2630
- name: Checkout repository
2731
uses: actions/checkout@v4
2832
- name: Install node
2933
uses: actions/setup-node@v4
3034
with:
31-
node-version: '22'
35+
node-version: ${{ matrix.node-version }}
3236
- name: Install dependencies
3337
run: npm ci
3438
- name: Test

‎README.md‎

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
A Node.js library for the [ipdata](https://ipdata.co) API. Look up IP addresses for geolocation, threat intelligence, company data, and more.
66

7-
Requires **Node.js 18** or later. Uses the native `fetch` API with zero HTTP dependencies.
7+
Requires **Node.js 20.18.1** or later. HTTP requests go through [undici](https://undici.nodejs.org), the HTTP client behind Node's built-in `fetch`, so that connections are pooled and reused across lookups and HTTP/2 is negotiated where available.
88

99
**Table of Contents**
1010

@@ -15,6 +15,7 @@ Requires **Node.js 18** or later. Uses the native `fetch` API with zero HTTP dep
1515
- [EU Endpoint](#eu-endpoint)
1616
- [Lookup](#lookup)
1717
- [Bulk Lookup](#bulk-lookup)
18+
- [Connection reuse and HTTP/2](#connection-reuse-and-http2)
1819
- [Response Fields](#response-fields)
1920
- [TypeScript](#typescript)
2021

@@ -140,6 +141,56 @@ With field filtering:
140141
const info = await ipdata.bulkLookup(['1.1.1.1', '1.0.0.1'], ['ip', 'city']);
141142
```
142143

144+
## Connection reuse and HTTP/2
145+
146+
An `IPData` instance keeps a persistent HTTPS connection pool (an [undici](https://undici.nodejs.org) `Agent`) for as long as it lives. Create one client per process (or one per API key or endpoint), keep it, and reuse it for every lookup: each new client pays a fresh TLS handshake. Requests to api.ipdata.co are multiplexed over HTTP/2 when the platform negotiates it, and fall back to HTTP/1.1 keep-alive otherwise.
147+
148+
Node.js runs your code on one thread, so a single instance can be shared by every request handler in the process; `worker_threads` and `cluster` workers each need their own instance, because connections cannot be shared across threads or processes. Idle HTTP/1.1 connections are closed after 60 seconds, HTTP/2 sessions are kept alive with a PING every 30 seconds, and neither keeps the event loop alive, so a script exits normally without any extra call. Call `await ipdata.close()` at shutdown when you want the pooled connections released immediately. If you pass your own `dispatcher`, you own it and `close()` leaves it open.
149+
150+
```js
151+
const ipdata = new IPData('<apiKey>');
152+
153+
// ... lookups ...
154+
155+
await ipdata.close();
156+
```
157+
158+
### HTTP options
159+
160+
The optional fourth constructor argument configures the transport:
161+
162+
```js
163+
const ipdata = new IPData('<apiKey>', undefined, undefined, {
164+
http2: true, // negotiate HTTP/2 via ALPN and fall back to HTTP/1.1 (default: true)
165+
keepAliveTimeout: 60000, // milliseconds an idle connection is kept before it is closed (default: 60000)
166+
});
167+
```
168+
169+
Use `dispatcher` to send requests through your own [undici dispatcher](https://undici.nodejs.org/#/docs/api/Dispatcher), for example to reach the API through a proxy or to intercept requests in tests. `http2` and `keepAliveTimeout` only apply to the pool the library creates itself.
170+
171+
```js
172+
import { EnvHttpProxyAgent, MockAgent, ProxyAgent } from 'undici';
173+
174+
// Explicit proxy
175+
const ipdata = new IPData('<apiKey>', undefined, undefined, {
176+
dispatcher: new ProxyAgent('http://proxy.example.com:8080'),
177+
});
178+
179+
// HTTP_PROXY, HTTPS_PROXY and NO_PROXY from the environment
180+
const ipdata = new IPData('<apiKey>', undefined, undefined, { dispatcher: new EnvHttpProxyAgent() });
181+
182+
// Tests: canned responses, no network
183+
const mockAgent = new MockAgent();
184+
mockAgent.disableNetConnect();
185+
mockAgent
186+
.get('https://api.ipdata.co')
187+
.intercept({ path: '/1.1.1.1?api-key=<apiKey>', method: 'GET' })
188+
.reply(200, { ip: '1.1.1.1', is_eu: false });
189+
const ipdata = new IPData('<apiKey>', undefined, undefined, { dispatcher: mockAgent });
190+
```
191+
192+
A request that fails because the server closed a pooled connection before any response byte arrived (connection reset or closed) is sent once more on a fresh connection; this also covers the `POST` made by `bulkLookup()`. HTTP error statuses are never retried.
193+
143194
## Response Fields
144195

145196
The following fields are available for use with `selectField` and `fields`:
@@ -159,5 +210,5 @@ The following fields are available for use with `selectField` and `fields`:
159210
The library is written in TypeScript and exports the following types:
160211

161212
```ts
162-
import IPData, { EU_BASE_URL, CacheConfig, LookupParams, LookupResponse } from 'ipdata';
213+
import IPData, { EU_BASE_URL, CacheConfig, HttpConfig, LookupParams, LookupResponse } from 'ipdata';
163214
```

‎package-lock.json‎

Lines changed: 14 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
{
22
"name": "ipdata",
3-
"version": "3.0.0",
3+
"version": "3.1.0",
44
"description": "JavaScript library to gather information for an ip using https://ipdata.co.",
55
"main": "./lib/ipdata.js",
66
"types": "./lib/ipdata.d.ts",
77
"engines": {
8-
"node": ">=18"
8+
"node": ">=20.18.1"
99
},
1010
"scripts": {
1111
"prebuild": "del lib",
@@ -25,7 +25,8 @@
2525
"homepage": "https://github.com/ipdata/node#readme",
2626
"dependencies": {
2727
"lru-cache": "^11.2.6",
28-
"tslib": "^2.8.1"
28+
"tslib": "^2.8.1",
29+
"undici": "^7.22.0"
2930
},
3031
"devDependencies": {
3132
"@types/jest": "^29.5.0",

0 commit comments

Comments
 (0)