├── .eslintignore ├── .eslintrc.cjs ├── .gitignore ├── .npmignore ├── Changelog.md ├── LICENSE.txt ├── README.md ├── __tests__ └── test.js ├── ava.config.cjs ├── axios-cached-dns-resolve.js ├── index.d.ts ├── index.js ├── logging.js ├── package-lock.json └── package.json /.eslintignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tcollinsworth/axios-cached-dns-resolve/e5d90cf3e024f80e3e19a9e6a5555653f9000898/.eslintignore -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'airbnb-base', 3 | plugins: [ 4 | 'import', 5 | ], 6 | rules: { 7 | semi: [2, 'never'], 8 | 'import/extensions': 0, 9 | 'import/no-extraneous-dependencies': ['error', { devDependencies: true }], 10 | 'max-len': 'off', 11 | 'import/prefer-default-export': 'off', 12 | 'no-use-before-define': 'off', 13 | 'no-return-assign': ['error', 'except-parens'], 14 | 'no-param-reassign': 'off', 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | tmp 3 | *.log 4 | npm-debug.log* 5 | 6 | node_modules 7 | dist 8 | 9 | *~ 10 | 11 | .nyc_output 12 | 13 | coverage 14 | 15 | *.npmrc 16 | 17 | .idea 18 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.npmrc 2 | .idea 3 | __tests__ 4 | .eslintignore 5 | .eslintrc.cjs 6 | .gitignore 7 | .npmignore 8 | ava.config.cjs 9 | .git 10 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [3.2.2] - 2022-09-06 4 | 5 | ### Fixed 6 | 7 | - Merged community PR (thanks matrec4) 'Adding a .d.ts file to declare the module for tsnode' [#29](https://github.com/tcollinsworth/axios-cached-dns-resolve/pull/29) 8 | 9 | 10 | ## [3.2.1] - 2022-09-06 11 | 12 | ### Fixed 13 | 14 | - Fixed bug were getDnsCacheEntries was returning generator from lru-cache to instead return array 15 | 16 | 17 | ## [3.2.0] - 2022-09-06 18 | 19 | ### Changed 20 | 21 | - Updated lru-cache to latest version 22 | 23 | ### Fixed 24 | 25 | - Fixed bug were fallback from dns.resolve failure to dns.lookup was not interpreting response array 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Troy T. Collinsworth 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # axios-cached-dns-resolve 2 | 3 | Axios uses node.js dns.lookup to resolve host names. 4 | dns.lookup is synchronous and executes on limited libuv thread pool. 5 | Every axios request will resolve the dns name in kubernetes, openshift, and cloud environments that intentionally set TTL low or to 0 for quick dynamic updates. 6 | The dns resolvers can be overwhelmed with the load. 7 | There is/was a bug in DNS resolutions that manifests as very long dns.lookups in node.js. 8 | 9 | From the kubernetes [documentation](https://kubernetes.io/docs/concepts/services-networking/service/#why-not-use-round-robin-dns) 10 | 11 | ``` 12 | Even if apps and libraries did proper re-resolution, the load of every client re-resolving DNS over and over would be difficult to manage. 13 | ``` 14 | 15 | This library uses dns.resolve and can optionally cache resolutions and round-robin among addresses. The cache size is configurable. 16 | If caching is enabled, a background thread will periodically refresh resolutions with dns.resolve rather than every request. 17 | There is an idle TTL that evicts background refresh if an address is no longer being used. 18 | This lib proxies through the OS resolution mechanism which may provide further caching. 19 | 20 | ## Objectives 21 | 22 | * Async requests - dns resolve vs lookup 23 | * Fast - local in-app memory cache lookup 24 | * Fresh - periodically (frequently) updated 25 | * Constant DNS load/latency vs random load/variable latency 26 | * Providing statistics and introspection 27 | 28 | ## Requirements 29 | 30 | ECMAScript module (esm), not native esm/.mjs with package.json type: module, requires esm 31 | 32 | Node 14+ 33 | 34 | ## Getting started 35 | 36 | ```console 37 | npm i -S axios-cached-dns-resolve 38 | ``` 39 | 40 | # Usage 41 | 42 | ```javascript 43 | import { registerInterceptor } from 'axios-cached-dns-resolve' 44 | 45 | const axiosClient = axios.create(config) 46 | 47 | registerInterceptor(axiosClient) 48 | 49 | ``` 50 | Use axiosClient as normal 51 | 52 | 53 | ## Configuration 54 | 55 | ```javascript 56 | const config = { 57 | disabled: process.env.AXIOS_DNS_DISABLE === 'true', 58 | dnsTtlMs: process.env.AXIOS_DNS_CACHE_TTL_MS || 5000, // when to refresh actively used dns entries (5 sec) 59 | cacheGraceExpireMultiplier: process.env.AXIOS_DNS_CACHE_EXPIRE_MULTIPLIER || 2, // maximum grace to use entry beyond TTL 60 | dnsIdleTtlMs: process.env.AXIOS_DNS_CACHE_IDLE_TTL_MS || 1000 * 60 * 60, // when to remove entry entirely if not being used (1 hour) 61 | backgroundScanMs: process.env.AXIOS_DNS_BACKGROUND_SCAN_MS || 2400, // how frequently to scan for expired TTL and refresh (2.4 sec) 62 | dnsCacheSize: process.env.AXIOS_DNS_CACHE_SIZE || 100, // maximum number of entries to keep in cache 63 | // pino logging options 64 | logging: { 65 | name: 'axios-cache-dns-resolve', 66 | // enabled: true, 67 | level: process.env.AXIOS_DNS_LOG_LEVEL || 'info', // default 'info' others trace, debug, info, warn, error, and fatal 68 | // timestamp: true, 69 | prettyPrint: process.env.NODE_ENV === 'DEBUG' || false, 70 | useLevelLabels: true, 71 | }, 72 | } 73 | ``` 74 | 75 | ## Statistics 76 | 77 | Statistics are available via 78 | 79 | ```javascript 80 | getStats() 81 | 82 | { 83 | "dnsEntries": 4, 84 | "refreshed": 375679, 85 | "hits": 128689, 86 | "misses": 393, 87 | "idleExpired": 279, 88 | "errors": 0, 89 | "lastError": 0, 90 | "lastErrorTs": 0 91 | } 92 | 93 | AND 94 | 95 | getDnsCacheEntries() 96 | 97 | [ 98 | { 99 | "host": "foo-service.domain.com", 100 | "ips": [ 101 | "51.210.235.165", 102 | "181.73.135.40" 103 | ], 104 | "nextIdx": 1, 105 | "lastUsedTs": 1604151366910, 106 | "updatedTs": 1604152691039 107 | }, 108 | ... 109 | ] 110 | ``` 111 | 112 | ### Express Statistics 113 | 114 | ```javascript 115 | import { getStats, getDnsCacheEntries } from 'axios-cached-dns-resolve' 116 | 117 | router.get('/axios-dns-cache-statistics', getAxiosDnsCacheStatistics) 118 | 119 | function getAxiosDnsCacheStatistics(req, resp) { 120 | resp.json(getStats()) 121 | } 122 | 123 | router.get('/axios-dns-cache-entries', getAxiosDnsCacheEntries) 124 | 125 | function getAxiosDnsCacheEntries(req, resp) { 126 | resp.json(getDnsCacheEntries()) 127 | } 128 | ``` 129 | -------------------------------------------------------------------------------- /__tests__/test.js: -------------------------------------------------------------------------------- 1 | import ava from 'ava' 2 | import delay from 'delay' 3 | import LRUCache from 'lru-cache' 4 | import axios from 'axios' 5 | import * as axiosCachingDns from '../index.js' 6 | 7 | const test = ava.serial 8 | 9 | let axiosClient 10 | 11 | test.beforeEach(() => { 12 | axiosCachingDns.config.dnsTtlMs = 1000 13 | axiosCachingDns.config.dnsIdleTtlMs = 5000 14 | axiosCachingDns.config.cacheGraceExpireMultiplier = 2 15 | axiosCachingDns.config.backgroundScanMs = 100 16 | 17 | axiosCachingDns.cacheConfig.ttl = (axiosCachingDns.config.dnsTtlMs * axiosCachingDns.config.cacheGraceExpireMultiplier) 18 | 19 | axiosCachingDns.config.cache = new LRUCache(axiosCachingDns.cacheConfig) 20 | 21 | axiosClient = axios.create({ 22 | timeout: 5000, 23 | // maxRedirects: 0, 24 | }) 25 | 26 | axiosCachingDns.registerInterceptor(axiosClient) 27 | 28 | axiosCachingDns.startBackgroundRefresh() 29 | axiosCachingDns.startPeriodicCachePrune() 30 | }) 31 | 32 | test.after.always(() => { 33 | axiosCachingDns.config.cache.clear() 34 | }) 35 | 36 | test('query google with baseURL and relative url', async (t) => { 37 | axiosCachingDns.registerInterceptor(axios) 38 | 39 | const { data } = await axios.get('/finance', { 40 | baseURL: 'http://www.google.com', 41 | // headers: { Authorization: `Basic ${basicauth}` }, 42 | }) 43 | t.truthy(data) 44 | t.is(1, axiosCachingDns.getStats().dnsEntries) 45 | t.is(1, axiosCachingDns.getStats().misses) 46 | 47 | const dnsCacheEntries = axiosCachingDns.getDnsCacheEntries() 48 | /* 49 | * [ 50 | * { 51 | * host: 'www.google.com', 52 | * ips: [ '142.250.190.68' ], 53 | * nextIdx: 1, 54 | * lastUsedTs: 1665100589485, 55 | * updatedTs: 1665100590488 56 | * } 57 | * ] 58 | */ 59 | t.truthy(Array.isArray(dnsCacheEntries)) 60 | t.truthy(Array.isArray(dnsCacheEntries[0].ips)) 61 | t.truthy(dnsCacheEntries[0].ips[0]) 62 | t.truthy(dnsCacheEntries[0].host) 63 | t.truthy(dnsCacheEntries[0].lastUsedTs) 64 | t.truthy(dnsCacheEntries[0].updatedTs) 65 | }) 66 | 67 | test('query google caches and after idle delay uncached', async (t) => { 68 | const resp = await axiosClient.get('http://amazon.com') 69 | t.truthy(resp.data) 70 | t.truthy(axiosCachingDns.config.cache.get('amazon.com')) 71 | await delay(6000) 72 | t.falsy(axiosCachingDns.config.cache.get('amazon.com')) 73 | 74 | const expectedStats = { 75 | dnsEntries: 0, 76 | // refreshed: 4, variable 77 | hits: 0, 78 | misses: 2, 79 | idleExpired: 1, 80 | errors: 0, 81 | lastError: 0, 82 | lastErrorTs: 0, 83 | } 84 | 85 | const stats = axiosCachingDns.getStats() 86 | delete stats.refreshed 87 | t.deepEqual(expectedStats, stats) 88 | }) 89 | 90 | test('query google caches and refreshes', async (t) => { 91 | await axiosClient.get('http://amazon.com') 92 | const { updatedTs } = axiosCachingDns.config.cache.get('amazon.com') 93 | const timeoutTime = Date.now() + 5000 94 | // eslint-disable-next-line no-constant-condition 95 | while (true) { 96 | const dnsEntry = axiosCachingDns.config.cache.get('amazon.com') 97 | if (!dnsEntry) t.fail('dnsEntry missing or expired') 98 | // console.log(dnsEntry) 99 | if (updatedTs !== dnsEntry.updatedTs) break 100 | if (Date.now() > timeoutTime) t.fail() 101 | // eslint-disable-next-line no-await-in-loop 102 | await delay(10) 103 | } 104 | 105 | const expectedStats = { 106 | dnsEntries: 1, 107 | // refreshed: 5, variable 108 | hits: 0, 109 | misses: 3, 110 | idleExpired: 1, 111 | errors: 0, 112 | lastError: 0, 113 | lastErrorTs: 0, 114 | } 115 | 116 | const stats = axiosCachingDns.getStats() 117 | delete stats.refreshed 118 | t.deepEqual(expectedStats, stats) 119 | }) 120 | 121 | test('query two services, caches and after one idle delay uncached', async (t) => { 122 | await axiosClient.get('http://amazon.com') 123 | 124 | await axiosClient.get('http://microsoft.com') 125 | const { lastUsedTs } = axiosCachingDns.config.cache.get('microsoft.com') 126 | t.is(1, axiosCachingDns.config.cache.get('microsoft.com').nextIdx) 127 | 128 | await axiosClient.get('http://microsoft.com') 129 | t.is(2, axiosCachingDns.config.cache.get('microsoft.com').nextIdx) 130 | 131 | t.truthy(lastUsedTs < axiosCachingDns.config.cache.get('microsoft.com').lastUsedTs) 132 | 133 | t.is(2, axiosCachingDns.config.cache.size) 134 | await axiosClient.get('http://microsoft.com') 135 | t.is(3, axiosCachingDns.config.cache.get('microsoft.com').nextIdx) 136 | 137 | t.falsy(lastUsedTs === axiosCachingDns.config.cache.get('microsoft.com').lastUsedTs) 138 | 139 | t.is(2, axiosCachingDns.config.cache.size) 140 | await delay(4000) 141 | t.is(1, axiosCachingDns.config.cache.size) 142 | await delay(2000) 143 | t.is(0, axiosCachingDns.config.cache.size) 144 | 145 | const expectedStats = { 146 | dnsEntries: 0, 147 | // refreshed: 17, variable 148 | hits: 2, 149 | misses: 5, 150 | idleExpired: 3, 151 | errors: 0, 152 | lastError: 0, 153 | lastErrorTs: 0, 154 | } 155 | 156 | const stats = axiosCachingDns.getStats() 157 | delete stats.refreshed 158 | t.deepEqual(expectedStats, stats) 159 | }) 160 | 161 | test('validate axios config not altered', async (t) => { 162 | const baseURL = 'http://microsoft.com' 163 | const axiosConfig = { baseURL } 164 | const custAxiosClient = axios.create(axiosConfig) 165 | 166 | axiosCachingDns.registerInterceptor(custAxiosClient) 167 | 168 | await custAxiosClient.get('/') 169 | t.is(baseURL, axiosConfig.baseURL) 170 | await custAxiosClient.get('/') 171 | t.is(baseURL, axiosConfig.baseURL) 172 | }) 173 | 174 | test('validate axios get config not altered', async (t) => { 175 | const url = 'http://microsoft.com' 176 | const custAxiosClient = axios.create() 177 | 178 | const reqConfig = { 179 | method: 'get', 180 | url, 181 | } 182 | 183 | axiosCachingDns.registerInterceptor(custAxiosClient) 184 | 185 | await custAxiosClient.get(url, reqConfig) 186 | t.is(url, reqConfig.url) 187 | await custAxiosClient.get(url, reqConfig) 188 | t.is(url, reqConfig.url) 189 | }) 190 | 191 | test('validate axios request config not altered', async (t) => { 192 | const url = 'http://microsoft.com' 193 | const custAxiosClient = axios.create() 194 | 195 | const reqConfig = { 196 | method: 'get', 197 | url, 198 | } 199 | 200 | axiosCachingDns.registerInterceptor(custAxiosClient) 201 | 202 | await custAxiosClient.request(reqConfig) 203 | t.is(url, reqConfig.url) 204 | await custAxiosClient.request(reqConfig) 205 | t.is(url, reqConfig.url) 206 | }) 207 | -------------------------------------------------------------------------------- /ava.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | files: [ 3 | '**/__tests__/**/*test*.js', 4 | ], 5 | failFast: true, 6 | verbose: true, 7 | failWithoutAssertions: false, 8 | require: [ 9 | 'ignore-styles', 10 | 'esm', 11 | ], 12 | } 13 | -------------------------------------------------------------------------------- /axios-cached-dns-resolve.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-plusplus */ 2 | import dns from 'dns' 3 | import URL from 'url' 4 | import net from 'net' 5 | import stringify from 'json-stringify-safe' 6 | import LRUCache from 'lru-cache' 7 | import util from 'util' 8 | import { init as initLogger } from './logging.js' 9 | 10 | const dnsResolve = util.promisify(dns.resolve) 11 | const dnsLookup = util.promisify(dns.lookup) 12 | 13 | export const config = { 14 | disabled: process.env.AXIOS_DNS_DISABLE === 'true', 15 | dnsTtlMs: process.env.AXIOS_DNS_CACHE_TTL_MS || 5000, // when to refresh actively used dns entries (5 sec) 16 | cacheGraceExpireMultiplier: process.env.AXIOS_DNS_CACHE_EXPIRE_MULTIPLIER || 2, // maximum grace to use entry beyond TTL 17 | dnsIdleTtlMs: process.env.AXIOS_DNS_CACHE_IDLE_TTL_MS || 1000 * 60 * 60, // when to remove entry entirely if not being used (1 hour) 18 | backgroundScanMs: process.env.AXIOS_DNS_BACKGROUND_SCAN_MS || 2400, // how frequently to scan for expired TTL and refresh (2.4 sec) 19 | dnsCacheSize: process.env.AXIOS_DNS_CACHE_SIZE || 100, // maximum number of entries to keep in cache 20 | // pino logging options 21 | logging: { 22 | name: 'axios-cache-dns-resolve', 23 | // enabled: true, 24 | level: process.env.AXIOS_DNS_LOG_LEVEL || 'info', // default 'info' others trace, debug, info, warn, error, and fatal 25 | // timestamp: true, 26 | prettyPrint: process.env.NODE_ENV === 'DEBUG' || false, 27 | formatters: { 28 | level(label/* , number */) { 29 | return { level: label } 30 | }, 31 | }, 32 | }, 33 | cache: undefined, 34 | } 35 | 36 | export const cacheConfig = { 37 | max: config.dnsCacheSize, 38 | ttl: (config.dnsTtlMs * config.cacheGraceExpireMultiplier), // grace for refresh 39 | } 40 | 41 | export const stats = { 42 | dnsEntries: 0, 43 | refreshed: 0, 44 | hits: 0, 45 | misses: 0, 46 | idleExpired: 0, 47 | errors: 0, 48 | lastError: 0, 49 | lastErrorTs: 0, 50 | } 51 | 52 | let log 53 | let backgroundRefreshId 54 | let cachePruneId 55 | 56 | init() 57 | 58 | export function init() { 59 | log = initLogger(config.logging) 60 | 61 | if (config.cache) return 62 | 63 | config.cache = new LRUCache(cacheConfig) 64 | 65 | startBackgroundRefresh() 66 | startPeriodicCachePrune() 67 | cachePruneId = setInterval(() => config.cache.purgeStale(), config.dnsIdleTtlMs) 68 | } 69 | 70 | export function reset() { 71 | if (backgroundRefreshId) clearInterval(backgroundRefreshId) 72 | if (cachePruneId) clearInterval(cachePruneId) 73 | } 74 | 75 | export function startBackgroundRefresh() { 76 | if (backgroundRefreshId) clearInterval(backgroundRefreshId) 77 | backgroundRefreshId = setInterval(backgroundRefresh, config.backgroundScanMs) 78 | } 79 | 80 | export function startPeriodicCachePrune() { 81 | if (cachePruneId) clearInterval(cachePruneId) 82 | cachePruneId = setInterval(() => config.cache.purgeStale(), config.dnsIdleTtlMs) 83 | } 84 | 85 | export function getStats() { 86 | stats.dnsEntries = config.cache.size 87 | return stats 88 | } 89 | 90 | export function getDnsCacheEntries() { 91 | return Array.from(config.cache.values()) 92 | } 93 | 94 | // const dnsEntry = { 95 | // host: 'www.amazon.com', 96 | // ips: [ 97 | // '52.54.40.141', 98 | // '34.205.98.207', 99 | // '3.82.118.51', 100 | // ], 101 | // nextIdx: 0, 102 | // lastUsedTs: 1555771516581, Date.now() 103 | // updatedTs: 1555771516581, 104 | // } 105 | 106 | export function registerInterceptor(axios) { 107 | if (config.disabled || !axios || !axios.interceptors) return // supertest 108 | axios.interceptors.request.use(async (reqConfig) => { 109 | try { 110 | let url 111 | if (reqConfig.baseURL) { 112 | url = URL.parse(reqConfig.baseURL) 113 | } else { 114 | url = URL.parse(reqConfig.url) 115 | } 116 | 117 | if (net.isIP(url.hostname)) return reqConfig // skip 118 | 119 | reqConfig.headers.Host = url.hostname // set hostname in header 120 | 121 | url.hostname = await getAddress(url.hostname) 122 | delete url.host // clear hostname 123 | 124 | if (reqConfig.baseURL) { 125 | reqConfig.baseURL = URL.format(url) 126 | } else { 127 | reqConfig.url = URL.format(url) 128 | } 129 | } catch (err) { 130 | recordError(err, `Error getAddress, ${err.message}`) 131 | } 132 | 133 | return reqConfig 134 | }) 135 | } 136 | 137 | export async function getAddress(host) { 138 | let dnsEntry = config.cache.get(host) 139 | if (dnsEntry) { 140 | ++stats.hits 141 | dnsEntry.lastUsedTs = Date.now() 142 | // eslint-disable-next-line no-plusplus 143 | const ip = dnsEntry.ips[dnsEntry.nextIdx++ % dnsEntry.ips.length] // round-robin 144 | config.cache.set(host, dnsEntry) 145 | return ip 146 | } 147 | ++stats.misses 148 | if (log.isLevelEnabled('debug')) log.debug(`cache miss ${host}`) 149 | 150 | const ips = await resolve(host) 151 | dnsEntry = { 152 | host, 153 | ips, 154 | nextIdx: 0, 155 | lastUsedTs: Date.now(), 156 | updatedTs: Date.now(), 157 | } 158 | // eslint-disable-next-line no-plusplus 159 | const ip = dnsEntry.ips[dnsEntry.nextIdx++ % dnsEntry.ips.length] // round-robin 160 | config.cache.set(host, dnsEntry) 161 | return ip 162 | } 163 | 164 | let backgroundRefreshing = false 165 | export async function backgroundRefresh() { 166 | if (backgroundRefreshing) return // don't start again if currently iterating slowly 167 | backgroundRefreshing = true 168 | try { 169 | config.cache.forEach(async (value, key) => { 170 | try { 171 | if (value.updatedTs + config.dnsTtlMs > Date.now()) { 172 | return // continue/skip 173 | } 174 | if (value.lastUsedTs + config.dnsIdleTtlMs <= Date.now()) { 175 | ++stats.idleExpired 176 | config.cache.delete(key) 177 | return // continue 178 | } 179 | 180 | const ips = await resolve(value.host) 181 | value.ips = ips 182 | value.updatedTs = Date.now() 183 | config.cache.set(key, value) 184 | ++stats.refreshed 185 | } catch (err) { 186 | // best effort 187 | recordError(err, `Error backgroundRefresh host: ${key}, ${stringify(value)}, ${err.message}`) 188 | } 189 | }) 190 | } catch (err) { 191 | // best effort 192 | recordError(err, `Error backgroundRefresh, ${err.message}`) 193 | } finally { 194 | backgroundRefreshing = false 195 | } 196 | } 197 | 198 | /** 199 | * 200 | * @param host 201 | * @returns {*[]} 202 | */ 203 | async function resolve(host) { 204 | let ips 205 | try { 206 | ips = await dnsResolve(host) 207 | } catch (e) { 208 | let lookupResp = await dnsLookup(host, { all: true }) // pass options all: true for all addresses 209 | lookupResp = extractAddresses(lookupResp) 210 | if (!Array.isArray(lookupResp) || lookupResp.length < 1) throw new Error(`fallback to dnsLookup returned no address ${host}`) 211 | ips = lookupResp 212 | } 213 | return ips 214 | } 215 | 216 | // dns.lookup 217 | // ***************** { address: '142.250.190.68', family: 4 } 218 | // , { all: true } /***************** [ { address: '142.250.190.68', family: 4 } ] 219 | 220 | function extractAddresses(lookupResp) { 221 | if (!Array.isArray(lookupResp)) throw new Error('lookup response did not contain array of addresses') 222 | return lookupResp.filter((e) => e.address != null).map((e) => e.address) 223 | } 224 | 225 | function recordError(err, errMesg) { 226 | ++stats.errors 227 | stats.lastError = err 228 | stats.lastErrorTs = new Date().toISOString() 229 | log.error(err, errMesg) 230 | } 231 | /* eslint-enable no-plusplus */ 232 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'axios-cached-dns-resolve'; 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export { 2 | config, 3 | cacheConfig, 4 | stats, 5 | init, 6 | reset, 7 | startBackgroundRefresh, 8 | startPeriodicCachePrune, 9 | getStats, 10 | getDnsCacheEntries, 11 | registerInterceptor, 12 | getAddress, 13 | backgroundRefresh, 14 | } from './axios-cached-dns-resolve.js' 15 | -------------------------------------------------------------------------------- /logging.js: -------------------------------------------------------------------------------- 1 | import pino from 'pino' 2 | 3 | let logger 4 | 5 | export function init(options) { 6 | return (logger = pino(options)) 7 | } 8 | 9 | export function getLogger() { 10 | return logger 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "axios-cached-dns-resolve", 3 | "version": "3.3.0", 4 | "main": "index", 5 | "description": "Caches dns resolutions made with async dns.resolve instead of default sync dns.lookup, refreshes in background", 6 | "scripts": { 7 | "watchUnit": "NODE_ENV=DEBUG ava --fail-fast -v **/__tests__/**/*test*.js --watch", 8 | "ava": "ava", 9 | "test": "npm run ava -timeout=2m", 10 | "watchLint": "esw . --ext=js --ext=mjs --ignore-path .gitignore --fix --watch", 11 | "lint": "eslint . --ext=js --ext=mjs --ignore-path .gitignore --fix" 12 | }, 13 | "engines": { 14 | "node": ">=14.0.0" 15 | }, 16 | "esm": { 17 | "force": true, 18 | "mode": "auto" 19 | }, 20 | "keywords": [ 21 | "axios", 22 | "dns", 23 | "cache", 24 | "resolve", 25 | "lookup" 26 | ], 27 | "author": "Troy Collinsworth", 28 | "license": "MIT", 29 | "repository": { 30 | "type": "git", 31 | "url": "git@github.com:tcollinsworth/axios-cached-dns-resolve.git" 32 | }, 33 | "devDependencies": { 34 | "ava": "^3.15.0", 35 | "axios": "^0.27.2", 36 | "body-parser": "^1.20.2", 37 | "delay": "^5.0.0", 38 | "eslint": "^8.44.0", 39 | "eslint-config-airbnb-base": "^15.0.0", 40 | "eslint-plugin-import": "^2.27.5", 41 | "eslint-watch": "^8.0.0", 42 | "esm": "^3.2.25", 43 | "express": "^4.18.2", 44 | "ignore-styles": "^5.0.1" 45 | }, 46 | "dependencies": { 47 | "json-stringify-safe": "^5.0.1", 48 | "lru-cache": "^7.18.3", 49 | "pino": "^8.14.1", 50 | "pino-pretty": "^10.0.1" 51 | } 52 | } 53 | --------------------------------------------------------------------------------