Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches:
- '**'
pull_request:

jobs:
lint:
Expand All @@ -22,13 +23,16 @@ jobs:

test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22, 24]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install node
uses: actions/setup-node@v4
with:
node-version: '22'
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm ci
- name: Test
Expand Down
69 changes: 66 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

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

Requires **Node.js 18** or later. Uses the native `fetch` API with zero HTTP dependencies.
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.

**Table of Contents**

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

Expand Down Expand Up @@ -140,24 +141,86 @@ With field filtering:
const info = await ipdata.bulkLookup(['1.1.1.1', '1.0.0.1'], ['ip', 'city']);
```

## Connection reuse and HTTP/2

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.

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.

```js
const ipdata = new IPData('<apiKey>');

// ... lookups ...

await ipdata.close();
```

### HTTP options

The optional fourth constructor argument configures the transport:

```js
const ipdata = new IPData('<apiKey>', undefined, undefined, {
http2: true, // negotiate HTTP/2 via ALPN and fall back to HTTP/1.1 (default: true)
keepAliveTimeout: 60000, // milliseconds an idle connection is kept before it is closed (default: 60000)
});
```

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.

```js
import { EnvHttpProxyAgent, MockAgent, ProxyAgent } from 'undici';

// Explicit proxy
const ipdata = new IPData('<apiKey>', undefined, undefined, {
dispatcher: new ProxyAgent('http://proxy.example.com:8080'),
});

// HTTP_PROXY, HTTPS_PROXY and NO_PROXY from the environment
const ipdata = new IPData('<apiKey>', undefined, undefined, { dispatcher: new EnvHttpProxyAgent() });

// Tests: canned responses, no network
const mockAgent = new MockAgent();
mockAgent.disableNetConnect();
mockAgent
.get('https://api.ipdata.co')
.intercept({ path: '/1.1.1.1?api-key=<apiKey>', method: 'GET' })
.reply(200, { ip: '1.1.1.1', is_eu: false });
const ipdata = new IPData('<apiKey>', undefined, undefined, { dispatcher: mockAgent });
```

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.

## Response Fields

The following fields are available for use with `selectField` and `fields`:

`ip`, `is_eu`, `city`, `region`, `region_code`, `country_name`, `country_code`, `continent_name`, `continent_code`, `latitude`, `longitude`, `asn`, `company`, `organisation`, `postal`, `calling_code`, `flag`, `emoji_flag`, `emoji_unicode`, `carrier`, `languages`, `currency`, `time_zone`, `threat`, `count`
`ip`, `is_eu`, `is_satellite`, `is_anycast`, `city`, `region`, `region_code`, `country_name`, `country_code`, `continent_name`, `continent_code`, `latitude`, `longitude`, `asn`, `company`, `organisation`, `postal`, `calling_code`, `flag`, `emoji_flag`, `emoji_unicode`, `carrier`, `languages`, `currency`, `time_zone`, `threat`, `count`

**`asn`** — `{ asn, name, domain, route, type }`

**`company`** — `{ name, domain, network, type }`

**`carrier`** — `{ name, mcc, mnc }`

**`is_satellite`** — `true` when the address belongs to a satellite internet provider. The API includes the key only when it is `true` and omits it otherwise, so treat a missing key as `false` (`info.is_satellite ?? false`).

**`is_anycast`** — `true` when the address is anycast. Also included only when `true`.

**`threat`** — `{ is_tor, is_icloud_relay, is_proxy, is_datacenter, is_anonymous, is_known_attacker, is_known_abuser, is_threat, is_bogon, blocklists }`

Each entry of `threat.blocklists` is `{ name, site, type, first_seen, last_seen, days_seen }`:

- **`first_seen`** — first day the address appeared on that blocklist, as a `YYYY-MM-DD` string (UTC)
- **`last_seen`** — most recent day the address appeared on that blocklist, same format
- **`days_seen`** — number of days the address has appeared on that blocklist (integer)

Some feeds, for example ipdata's own `vpn` list, do not report these three, so an entry may contain only `name`, `site` and `type`. The dates are passed through as strings.

## TypeScript

The library is written in TypeScript and exports the following types:

```ts
import IPData, { EU_BASE_URL, CacheConfig, LookupParams, LookupResponse } from 'ipdata';
import IPData, { EU_BASE_URL, CacheConfig, HttpConfig, LookupParams, LookupResponse } from 'ipdata';
```
18 changes: 14 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"name": "ipdata",
"version": "3.0.0",
"version": "3.1.0",
"description": "JavaScript library to gather information for an ip using https://ipdata.co.",
"main": "./lib/ipdata.js",
"types": "./lib/ipdata.d.ts",
"engines": {
"node": ">=18"
"node": ">=20.18.1"
},
"scripts": {
"prebuild": "del lib",
Expand All @@ -25,7 +25,8 @@
"homepage": "https://github.com/ipdata/node#readme",
"dependencies": {
"lru-cache": "^11.2.6",
"tslib": "^2.8.1"
"tslib": "^2.8.1",
"undici": "^7.22.0"
},
"devDependencies": {
"@types/jest": "^29.5.0",
Expand Down
Loading
Loading