@maxmind/geoip2-node
Version:
Node.js API for GeoIP2 webservice client and database reader
150 lines (149 loc) • 5.26 kB
JavaScript
import * as mmdb from 'maxmind';
import packageInfo from '../package.json' with { type: 'json' };
import { ValueError, WebServiceError } from './errors.js';
import * as models from './models/index.js';
const invalidResponseBody = {
code: 'INVALID_RESPONSE_BODY',
error: 'Received an invalid or unparseable response body',
};
const clientError = (properties, options) => new WebServiceError(properties, options);
const isErrorBody = (data) => typeof data === 'object' &&
data !== null &&
typeof data.code === 'string' &&
data.code !== '' &&
typeof data.error === 'string' &&
data.error !== '';
export default class WebServiceClient {
accountID;
licenseKey;
timeout = 3000;
host = 'geoip.maxmind.com';
fetcher = fetch;
constructor(accountID, licenseKey, options) {
this.accountID = accountID;
this.licenseKey = licenseKey;
if (options === undefined || options === null) {
return;
}
if (typeof options === 'object') {
if (options.fetcher !== undefined) {
if (typeof options.fetcher !== 'function') {
throw new ValueError('`fetcher` must be a function');
}
this.fetcher = options.fetcher;
}
if (options.host !== undefined) {
if (typeof options.host !== 'string') {
throw new ValueError('`host` must be a string');
}
this.host = options.host;
}
if (options.timeout !== undefined) {
if (typeof options.timeout !== 'number' ||
!Number.isFinite(options.timeout)) {
throw new ValueError('`timeout` must be a finite number');
}
this.timeout = options.timeout;
}
return;
}
this.timeout = options;
}
city(ipAddress) {
return this.responseFor('city', ipAddress, models.City);
}
country(ipAddress) {
return this.responseFor('country', ipAddress, models.Country);
}
insights(ipAddress) {
return this.responseFor('insights', ipAddress, models.Insights);
}
async responseFor(path, ipAddress, modelClass) {
const parsedPath = `/geoip/v2.1/${path}/${ipAddress}`;
const url = `https://${this.host}${parsedPath}`;
if (!mmdb.validate(ipAddress)) {
throw clientError({
code: 'IP_ADDRESS_INVALID',
error: 'The IP address provided is invalid',
url,
});
}
const options = {
headers: {
Accept: 'application/json',
Authorization: 'Basic ' + btoa(`${this.accountID}:${this.licenseKey}`),
'User-Agent': `GeoIP2-node/${packageInfo.version}`,
},
method: 'GET',
signal: AbortSignal.timeout(this.timeout),
};
let response;
try {
response = await this.fetcher(url, options);
}
catch (err) {
const error = err instanceof Error || err instanceof DOMException
? err
: new Error(String(err));
if (error.name === 'TimeoutError') {
throw clientError({
code: 'NETWORK_TIMEOUT',
error: 'The request timed out',
url,
}, { cause: error });
}
const causeDetail = error.cause instanceof Error ? `: ${error.cause.message}` : '';
throw clientError({
code: 'FETCH_ERROR',
error: `${error.name} - ${error.message}${causeDetail}`,
url,
}, { cause: error });
}
if (!response.ok) {
throw await this.handleError(response, url);
}
let data;
try {
data = await response.json();
}
catch (err) {
throw clientError({ ...invalidResponseBody, url }, { cause: err });
}
return new modelClass(data);
}
async handleError(response, url) {
const status = response.status;
if (status && status >= 500 && status < 600) {
return clientError({
code: 'SERVER_ERROR',
error: `Received a server error with HTTP status code: ${status}`,
status,
url,
});
}
if (status && (status < 400 || status >= 600)) {
return clientError({
code: 'HTTP_STATUS_CODE_ERROR',
error: `Received an unexpected HTTP status code: ${status}`,
status,
url,
});
}
let data;
try {
data = await response.json();
}
catch (err) {
return clientError({ ...invalidResponseBody, status, url }, { cause: err });
}
if (!isErrorBody(data)) {
return clientError({ ...invalidResponseBody, status, url });
}
return new WebServiceError({
code: data.code,
error: data.error,
status,
url,
});
}
}