UNPKG

@maxmind/geoip2-node

Version:

Node.js API for GeoIP2 webservice client and database reader

150 lines (149 loc) 5.26 kB
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, }); } }