Repository: axios/axios
Pull request #5097 (2022-10-11 10:41:15)

fix: restore proxy config backwards compatibility with 0.x
This pull request fixes https://github.com/axios/axios/issues/5079.

Since 1.x, the proxy configuration has been backwards incompatible with 0.x versions:
* `hostname` instead of `host`
* `protocol` value requires a trailing `:` character

This pull request essentially makes Axios compatible with both 1.x- and 0.x-style configurations. One caveat: if both `hostname` and `host` are given, `hostname` takes precedence. This behavior is consistent with the Node.js `http` module [0].

[0] https://nodejs.org/api/http.html#httprequesturl-options-callback


Patch:
diff --git a/README.md b/README.md
index 4a5af19..9ffde41 100755
--- a/README.md
+++ b/README.md
@@ -482,6 +482,7 @@ These are the available config options for making requests. Only the `url` is re
   proxy: {
     protocol: 'https',
     host: '127.0.0.1',
+    // hostname: '127.0.0.1' // Takes precedence over 'host' if both are defined
     port: 9000,
     auth: {
       username: 'mikeymike',
diff --git a/lib/adapters/http.js b/lib/adapters/http.js
index 35722b3..9e56fda 100755
--- a/lib/adapters/http.js
+++ b/lib/adapters/http.js
@@ -51,7 +51,7 @@ function dispatchBeforeRedirect(options) {
  * If the proxy or config afterRedirects functions are defined, call them with the options
  *
  * @param {http.ClientRequestArgs} options
- * @param {AxiosProxyConfig} configProxy
+ * @param {AxiosProxyConfig} configProxy configuration from Axios options object
  * @param {string} location
  *
  * @returns {http.ClientRequestArgs}
@@ -82,13 +82,14 @@ function setProxy(options, configProxy, location) {
     }
 
     options.headers.host = options.hostname + (options.port ? ':' + options.port : '');
-    options.hostname = proxy.hostname;
+    const proxyHost = proxy.hostname || proxy.host;
+    options.hostname = proxyHost;
     // Replace 'host' since options is not a URL object
-    options.host = proxy.hostname;
+    options.host = proxyHost;
     options.port = proxy.port;
     options.path = location;
     if (proxy.protocol) {
-      options.protocol = proxy.protocol;
+      options.protocol = proxy.protocol.includes(':') ? proxy.protocol : `${proxy.protocol}:`;
     }
   }
 
@@ -586,3 +587,5 @@ export default function httpAdapter(config) {
     }
   });
 }
+
+export const __setProxy = setProxy;
\ No newline at end of file


Test patch:
diff --git a/test/unit/adapters/http.js b/test/unit/adapters/http.js
index ed43446..c92b039 100644
--- a/test/unit/adapters/http.js
+++ b/test/unit/adapters/http.js
@@ -20,6 +20,7 @@ const isBlobSupported = typeof Blob !== 'undefined';
 import {Throttle} from 'stream-throttle';
 import devNull from 'dev-null';
 import {AbortController} from 'abortcontroller-polyfill/dist/cjs-ponyfill.js';
+import {__setProxy} from "../../../lib/adapters/http.js";
 
 const __filename = url.fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);
@@ -1236,6 +1237,38 @@ describe('supports http with nodejs', function () {
     });
   });
 
+  context('different options for direct proxy configuration (without env variables)', () => {
+    const destination = 'www.example.com';
+
+    const testCases = [{
+      description: 'hostname and trailing colon in protocol',
+      proxyConfig: { hostname: '127.0.0.1', protocol: 'http:', port: 80 },
+      expectedOptions: { host: '127.0.0.1', protocol: 'http:', port: 80, path: destination }
+    }, {
+      description: 'hostname and no trailing colon in protocol',
+      proxyConfig: { hostname: '127.0.0.1', protocol: 'http', port: 80 },
+      expectedOptions: { host: '127.0.0.1', protocol: 'http:', port: 80, path: destination }
+    }, {
+      description: 'both hostname and host -> hostname takes precedence',
+      proxyConfig: { hostname: '127.0.0.1', host: '0.0.0.0', protocol: 'http', port: 80 },
+      expectedOptions: { host: '127.0.0.1', protocol: 'http:', port: 80, path: destination }
+    }, {
+      description: 'only host and https protocol',
+      proxyConfig: { host: '0.0.0.0', protocol: 'https', port: 80 },
+      expectedOptions: { host: '0.0.0.0', protocol: 'https:', port: 80, path: destination }
+    }];
+
+    for (const test of testCases) {
+      it(test.description, () => {
+        const options = { headers: {}, beforeRedirects: {} };
+        __setProxy(options, test.proxyConfig, destination);
+        for (const [key, expected] of Object.entries(test.expectedOptions)) {
+          assert.equal(options[key], expected);
+        }
+      });
+    }
+  });
+
   it('should support cancel', function (done) {
     var source = axios.CancelToken.source();
     server = http.createServer(function (req, res) {