diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dccc661fa..88e5605ad4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -514,4 +514,4 @@ - [Marco Weber](https://github.com/mrcwbr) - [Luca Pizzini](https://github.com/lpizzinidev) - [Willian Agostini](https://github.com/WillianAgostini) -- [Huyen Nguyen](https://github.com/huyenltnguyen) \ No newline at end of file +- [Huyen Nguyen](https://github.com/huyenltnguyen) diff --git a/index.d.cts b/index.d.cts index 6d564e2636..138c09ee2b 100644 --- a/index.d.cts +++ b/index.d.cts @@ -403,6 +403,9 @@ declare namespace axios { FormData?: new (...args: any[]) => object; }; formSerializer?: FormSerializerOptions; + family?: 4 | 6 | undefined; + lookup?: ((hostname: string, options: object, cb: (err: Error | null, address: string, family: number) => void) => void) | + ((hostname: string, options: object) => Promise<[address: string, family: number] | string>); } // Alias diff --git a/index.d.ts b/index.d.ts index 220dcb3d7b..7be345c21e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -329,7 +329,7 @@ export interface AxiosRequestConfig { maxBodyLength?: number; maxRedirects?: number; maxRate?: number | [MaxUploadRate, MaxDownloadRate]; - beforeRedirect?: (options: Record, responseDetails: {headers: Record}) => void; + beforeRedirect?: (options: Record, responseDetails: { headers: Record }) => void; socketPath?: string | null; transport?: any; httpAgent?: any; @@ -344,6 +344,9 @@ export interface AxiosRequestConfig { FormData?: new (...args: any[]) => object; }; formSerializer?: FormSerializerOptions; + family?: 4 | 6 | undefined; + lookup?: ((hostname: string, options: object, cb: (err: Error | null, address: string, family: number) => void) => void) | + ((hostname: string, options: object) => Promise<[address: string, family: number] | string>); } // Alias diff --git a/lib/adapters/http.js b/lib/adapters/http.js index 9ca70668e9..bf6c2fffcb 100755 --- a/lib/adapters/http.js +++ b/lib/adapters/http.js @@ -23,6 +23,7 @@ import EventEmitter from 'events'; import formDataToStream from "../helpers/formDataToStream.js"; import readBlob from "../helpers/readBlob.js"; import ZlibHeaderTransformStream from '../helpers/ZlibHeaderTransformStream.js'; +import callbackify from "../helpers/callbackify.js"; const zlibOptions = { flush: zlib.constants.Z_SYNC_FLUSH, @@ -146,13 +147,24 @@ const wrapAsync = (asyncExecutor) => { /*eslint consistent-return:0*/ export default isHttpAdapterSupported && function httpAdapter(config) { return wrapAsync(async function dispatchHttpRequest(resolve, reject, onDone) { - let {data} = config; + let {data, lookup, family} = config; const {responseType, responseEncoding} = config; const method = config.method.toUpperCase(); let isDone; let rejected = false; let req; + if (lookup && utils.isAsyncFn(lookup)) { + lookup = callbackify(lookup, (entry) => { + if(utils.isString(entry)) { + entry = [entry, entry.indexOf('.') < 0 ? 6 : 4] + } else if (!utils.isArray(entry)) { + throw new TypeError('lookup async function must return an array [ip: string, family: number]]') + } + return entry; + }) + } + // temporary internal emitter until the AxiosRequest class will be implemented const emitter = new EventEmitter(); @@ -378,6 +390,8 @@ export default isHttpAdapterSupported && function httpAdapter(config) { agents: { http: config.httpAgent, https: config.httpsAgent }, auth, protocol, + family, + lookup, beforeRedirect: dispatchBeforeRedirect, beforeRedirects: {} }; diff --git a/lib/helpers/callbackify.js b/lib/helpers/callbackify.js new file mode 100644 index 0000000000..4603badc31 --- /dev/null +++ b/lib/helpers/callbackify.js @@ -0,0 +1,16 @@ +import utils from "../utils.js"; + +const callbackify = (fn, reducer) => { + return utils.isAsyncFn(fn) ? function (...args) { + const cb = args.pop(); + fn.apply(this, args).then((value) => { + try { + reducer ? cb(null, ...reducer(value)) : cb(null, value); + } catch (err) { + cb(err); + } + }, cb); + } : fn; +} + +export default callbackify; diff --git a/lib/utils.js b/lib/utils.js index 80ae34c0c4..6beea04885 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -662,6 +662,15 @@ const toJSONObject = (obj) => { return visit(obj, 0); } +const [isPlainFunction, isAsyncFn, isGeneratorFn, isAsyncGeneratorFn] = ( + (...fns) => fns.map( + ({constructor})=> (thing) => thing && typeof thing === 'function' && thing.constructor === constructor + ) +)(()=> {}, async()=>{}, function*(){}, async function*(){}); + +const isThenable = (thing) => + thing && (isObject(thing) || isFunction(thing)) && isFunction(thing.then) && isFunction(thing.catch); + export default { isArray, isArrayBuffer, @@ -711,5 +720,10 @@ export default { ALPHABET, generateString, isSpecCompliantForm, - toJSONObject + toJSONObject, + isAsyncFn, + isGeneratorFn, + isAsyncGeneratorFn, + isPlainFunction, + isThenable }; diff --git a/test/module/typings/esm/index.ts b/test/module/typings/esm/index.ts index d7d4cdd97c..938652e7c7 100644 --- a/test/module/typings/esm/index.ts +++ b/test/module/typings/esm/index.ts @@ -627,3 +627,17 @@ for (const [header, value] of headers) { } }); } + +// lookup +axios.get('/user', { + lookup: (hostname: string, opt: object, cb: (err: Error | null, address: string, family: number) => void) => { + cb(null, '127.0.0.1', 4); + } +}); + +// lookup async +axios.get('/user', { + lookup: (hostname: string, opt: object) => { + return ['127.0.0.1', 4]; + } +}); diff --git a/test/unit/adapters/http.js b/test/unit/adapters/http.js index 769e4692da..a588ab8fbe 100644 --- a/test/unit/adapters/http.js +++ b/test/unit/adapters/http.js @@ -56,6 +56,8 @@ var noop = ()=> {}; const LOCAL_SERVER_URL = 'http://localhost:4444'; +const SERVER_HANDLER_STREAM_ECHO = (req, res) => req.pipe(res); + function startHTTPServer(options) { const {handler, useBuffering = false, rate = undefined, port = 4444} = typeof options === 'function' ? { @@ -2117,4 +2119,63 @@ describe('supports http with nodejs', function () { assert.strictEqual(data, '/?foo'); }); + + describe('DNS', function() { + it('should support custom DNS lookup function', async function () { + server = await startHTTPServer(SERVER_HANDLER_STREAM_ECHO); + + const payload = 'test'; + + let isCalled = false; + + const {data} = await axios.post(`http://fake-name.axios:4444`, payload,{ + lookup: (hostname, opt, cb) => { + isCalled = true; + cb(null, '127.0.0.1', 4); + } + }); + + assert.ok(isCalled); + + assert.strictEqual(data, payload); + }); + + it('should support custom DNS lookup function (async)', async function () { + server = await startHTTPServer(SERVER_HANDLER_STREAM_ECHO); + + const payload = 'test'; + + let isCalled = false; + + const {data} = await axios.post(`http://fake-name.axios:4444`, payload,{ + lookup: async (hostname, opt) => { + isCalled = true; + return ['127.0.0.1', 4]; + } + }); + + assert.ok(isCalled); + + assert.strictEqual(data, payload); + }); + + it('should support custom DNS lookup function that returns only IP address (async)', async function () { + server = await startHTTPServer(SERVER_HANDLER_STREAM_ECHO); + + const payload = 'test'; + + let isCalled = false; + + const {data} = await axios.post(`http://fake-name.axios:4444`, payload,{ + lookup: async (hostname, opt) => { + isCalled = true; + return '127.0.0.1'; + } + }); + + assert.ok(isCalled); + + assert.strictEqual(data, payload); + }); + }); });