axios 1.15.2 → 1.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +103 -6
  2. package/README.md +396 -25
  3. package/dist/axios.js +1455 -1109
  4. package/dist/axios.js.map +1 -1
  5. package/dist/axios.min.js +3 -3
  6. package/dist/axios.min.js.map +1 -1
  7. package/dist/browser/axios.cjs +1569 -1174
  8. package/dist/browser/axios.cjs.map +1 -1
  9. package/dist/esm/axios.js +1569 -1173
  10. package/dist/esm/axios.js.map +1 -1
  11. package/dist/esm/axios.min.js +2 -2
  12. package/dist/esm/axios.min.js.map +1 -1
  13. package/dist/node/axios.cjs +1395 -915
  14. package/dist/node/axios.cjs.map +1 -1
  15. package/index.d.cts +25 -13
  16. package/index.d.ts +21 -4
  17. package/index.js +2 -0
  18. package/lib/adapters/adapters.js +4 -2
  19. package/lib/adapters/fetch.js +131 -11
  20. package/lib/adapters/http.js +298 -69
  21. package/lib/adapters/xhr.js +8 -3
  22. package/lib/core/Axios.js +7 -3
  23. package/lib/core/AxiosError.js +86 -1
  24. package/lib/core/AxiosHeaders.js +4 -33
  25. package/lib/core/dispatchRequest.js +19 -7
  26. package/lib/core/mergeConfig.js +6 -3
  27. package/lib/core/settle.js +7 -11
  28. package/lib/defaults/index.js +1 -1
  29. package/lib/env/data.js +1 -1
  30. package/lib/helpers/buildURL.js +1 -1
  31. package/lib/helpers/composeSignals.js +48 -47
  32. package/lib/helpers/cookies.js +14 -2
  33. package/lib/helpers/estimateDataURLDecodedBytes.js +28 -1
  34. package/lib/helpers/formDataToJSON.js +1 -1
  35. package/lib/helpers/formDataToStream.js +1 -1
  36. package/lib/helpers/fromDataURI.js +18 -5
  37. package/lib/helpers/parseProtocol.js +1 -1
  38. package/lib/helpers/progressEventReducer.js +3 -0
  39. package/lib/helpers/resolveConfig.js +33 -17
  40. package/lib/helpers/sanitizeHeaderValue.js +60 -0
  41. package/lib/helpers/shouldBypassProxy.js +26 -1
  42. package/lib/helpers/validator.js +1 -1
  43. package/lib/utils.js +35 -22
  44. package/package.json +19 -24
@@ -3,6 +3,7 @@ import settle from '../core/settle.js';
3
3
  import buildFullPath from '../core/buildFullPath.js';
4
4
  import buildURL from '../helpers/buildURL.js';
5
5
  import { getProxyForUrl } from 'proxy-from-env';
6
+ import HttpsProxyAgent from 'https-proxy-agent';
6
7
  import http from 'http';
7
8
  import https from 'https';
8
9
  import http2 from 'http2';
@@ -25,6 +26,7 @@ import readBlob from '../helpers/readBlob.js';
25
26
  import ZlibHeaderTransformStream from '../helpers/ZlibHeaderTransformStream.js';
26
27
  import callbackify from '../helpers/callbackify.js';
27
28
  import shouldBypassProxy from '../helpers/shouldBypassProxy.js';
29
+ import { toByteStringHeaderObject } from '../helpers/sanitizeHeaderValue.js';
28
30
  import {
29
31
  progressEventReducer,
30
32
  progressEventDecorator,
@@ -47,16 +49,85 @@ const isBrotliSupported = utils.isFunction(zlib.createBrotliDecompress);
47
49
  const { http: httpFollow, https: httpsFollow } = followRedirects;
48
50
 
49
51
  const isHttps = /https:?/;
52
+ const FORM_DATA_CONTENT_HEADERS = ['content-type', 'content-length'];
53
+
54
+ function setFormDataHeaders(headers, formHeaders, policy) {
55
+ if (policy !== 'content-only') {
56
+ headers.set(formHeaders);
57
+ return;
58
+ }
59
+
60
+ Object.entries(formHeaders).forEach(([key, val]) => {
61
+ if (FORM_DATA_CONTENT_HEADERS.includes(key.toLowerCase())) {
62
+ headers.set(key, val);
63
+ }
64
+ });
65
+ }
50
66
 
51
67
  // Symbols used to bind a single 'error' listener to a pooled socket and track
52
68
  // the request currently owning that socket across keep-alive reuse (issue #10780).
53
69
  const kAxiosSocketListener = Symbol('axios.http.socketListener');
54
70
  const kAxiosCurrentReq = Symbol('axios.http.currentReq');
55
71
 
72
+ // Tags HttpsProxyAgent instances installed by setProxy() so the redirect path
73
+ // can strip them without clobbering a user-supplied agent that happens to be
74
+ // an HttpsProxyAgent.
75
+ const kAxiosInstalledTunnel = Symbol('axios.http.installedTunnel');
76
+
77
+ // Cache of CONNECT-tunneling agents keyed by proxy config so repeat requests
78
+ // through the same proxy reuse a single agent (and its socket pool). The
79
+ // keyspace is bounded by the set of distinct proxy configs the process uses,
80
+ // so unbounded growth is not a concern in practice.
81
+ const tunnelingAgentCache = new Map();
82
+ const tunnelingAgentCacheUser = new WeakMap();
83
+
84
+ function getTunnelingAgent(agentOptions, userHttpsAgent) {
85
+ const key =
86
+ agentOptions.protocol +
87
+ '//' +
88
+ agentOptions.hostname +
89
+ ':' +
90
+ (agentOptions.port || '') +
91
+ '#' +
92
+ (agentOptions.auth || '');
93
+ const cache = userHttpsAgent
94
+ ? (tunnelingAgentCacheUser.get(userHttpsAgent) ||
95
+ tunnelingAgentCacheUser.set(userHttpsAgent, new Map()).get(userHttpsAgent))
96
+ : tunnelingAgentCache;
97
+ let agent = cache.get(key);
98
+ if (agent) return agent;
99
+ // Forward the user's TLS options (custom CA, rejectUnauthorized, client cert,
100
+ // etc.) into the tunneling agent so they apply to the origin TLS upgrade
101
+ // performed after CONNECT. Our proxy fields take precedence on conflict.
102
+ const merged = userHttpsAgent && userHttpsAgent.options
103
+ ? { ...userHttpsAgent.options, ...agentOptions }
104
+ : agentOptions;
105
+ agent = new HttpsProxyAgent(merged);
106
+ agent[kAxiosInstalledTunnel] = true;
107
+ cache.set(key, agent);
108
+ return agent;
109
+ }
110
+
56
111
  const supportedProtocols = platform.protocols.map((protocol) => {
57
112
  return protocol + ':';
58
113
  });
59
114
 
115
+ // Node's WHATWG URL parser returns `username` and `password` percent-encoded.
116
+ // Decode before composing the `auth` option so credentials such as
117
+ // `my%40email.com:pass` are sent as `my@email.com:pass`. Falls back to the
118
+ // original value for malformed input so a bad encoding never throws.
119
+ const decodeURIComponentSafe = (value) => {
120
+ if (!utils.isString(value)) {
121
+ return value;
122
+ }
123
+
124
+ try {
125
+ return decodeURIComponent(value);
126
+ } catch (error) {
127
+ return value;
128
+ }
129
+ };
130
+
60
131
  const flushOnFinish = (stream, [throttled, flush]) => {
61
132
  stream.on('end', flush).on('error', flush);
62
133
 
@@ -176,12 +247,12 @@ const http2Sessions = new Http2Sessions();
176
247
  *
177
248
  * @returns {Object<string, any>}
178
249
  */
179
- function dispatchBeforeRedirect(options, responseDetails) {
250
+ function dispatchBeforeRedirect(options, responseDetails, requestDetails) {
180
251
  if (options.beforeRedirects.proxy) {
181
252
  options.beforeRedirects.proxy(options);
182
253
  }
183
254
  if (options.beforeRedirects.config) {
184
- options.beforeRedirects.config(options, responseDetails);
255
+ options.beforeRedirects.config(options, responseDetails, requestDetails);
185
256
  }
186
257
  }
187
258
 
@@ -194,7 +265,7 @@ function dispatchBeforeRedirect(options, responseDetails) {
194
265
  *
195
266
  * @returns {http.ClientRequestArgs}
196
267
  */
197
- function setProxy(options, configProxy, location) {
268
+ function setProxy(options, configProxy, location, isRedirect, configHttpsAgent) {
198
269
  let proxy = configProxy;
199
270
  if (!proxy && proxy !== false) {
200
271
  const proxyUrl = getProxyForUrl(location);
@@ -204,43 +275,150 @@ function setProxy(options, configProxy, location) {
204
275
  }
205
276
  }
206
277
  }
278
+ // On redirect re-invocation, strip any stale Proxy-Authorization header carried
279
+ // over from the prior request (e.g. new target no longer uses a proxy, or uses
280
+ // a different proxy). Skip on the initial request so user-supplied headers are
281
+ // preserved. Header names are case-insensitive, so remove every case variant.
282
+ if (isRedirect && options.headers) {
283
+ for (const name of Object.keys(options.headers)) {
284
+ if (name.toLowerCase() === 'proxy-authorization') {
285
+ delete options.headers[name];
286
+ }
287
+ }
288
+ }
289
+ // Strip any tunneling agent we installed for the previous hop so a redirect
290
+ // that drops the proxy or crosses an HTTPS↔HTTP boundary doesn't reuse a
291
+ // stale one. Match on our Symbol marker so a user-supplied HttpsProxyAgent
292
+ // (which won't carry the marker) is left alone.
293
+ if (isRedirect && options.agent && options.agent[kAxiosInstalledTunnel]) {
294
+ options.agent = undefined;
295
+ }
207
296
  if (proxy) {
297
+ // Read proxy fields without traversing the prototype chain. URL instances expose
298
+ // username/password/hostname/host/port/protocol via getters on URL.prototype (so
299
+ // direct reads are shielded), but plain object proxies — and the `auth` field
300
+ // (which URL does not expose) — must be guarded so a polluted Object.prototype
301
+ // (e.g. Object.prototype.auth = { username, password }) cannot inject
302
+ // attacker-controlled credentials into the Proxy-Authorization header or
303
+ // redirect proxying to an attacker-controlled host.
304
+ const isProxyURL = proxy instanceof URL;
305
+ const readProxyField = (key) =>
306
+ isProxyURL || utils.hasOwnProp(proxy, key) ? proxy[key] : undefined;
307
+
308
+ const proxyUsername = readProxyField('username');
309
+ const proxyPassword = readProxyField('password');
310
+ let proxyAuth = utils.hasOwnProp(proxy, 'auth') ? proxy.auth : undefined;
311
+
208
312
  // Basic proxy authorization
209
- if (proxy.username) {
210
- proxy.auth = (proxy.username || '') + ':' + (proxy.password || '');
313
+ if (proxyUsername) {
314
+ proxyAuth = (proxyUsername || '') + ':' + (proxyPassword || '');
211
315
  }
212
316
 
213
- if (proxy.auth) {
214
- // Support proxy auth object form
215
- const validProxyAuth = Boolean(proxy.auth.username || proxy.auth.password);
317
+ if (proxyAuth) {
318
+ // Support proxy auth object form. Read sub-fields via own-prop checks so a
319
+ // plain object inheriting from polluted Object.prototype cannot leak creds.
320
+ const authIsObject = typeof proxyAuth === 'object';
321
+ const authUsername =
322
+ authIsObject && utils.hasOwnProp(proxyAuth, 'username') ? proxyAuth.username : undefined;
323
+ const authPassword =
324
+ authIsObject && utils.hasOwnProp(proxyAuth, 'password') ? proxyAuth.password : undefined;
325
+ const validProxyAuth = Boolean(authUsername || authPassword);
216
326
 
217
327
  if (validProxyAuth) {
218
- proxy.auth = (proxy.auth.username || '') + ':' + (proxy.auth.password || '');
219
- } else if (typeof proxy.auth === 'object') {
328
+ proxyAuth = (authUsername || '') + ':' + (authPassword || '');
329
+ } else if (authIsObject) {
220
330
  throw new AxiosError('Invalid proxy authorization', AxiosError.ERR_BAD_OPTION, { proxy });
221
331
  }
222
-
223
- const base64 = Buffer.from(proxy.auth, 'utf8').toString('base64');
224
-
225
- options.headers['Proxy-Authorization'] = 'Basic ' + base64;
226
332
  }
227
333
 
228
- options.headers.host = options.hostname + (options.port ? ':' + options.port : '');
229
- const proxyHost = proxy.hostname || proxy.host;
230
- options.hostname = proxyHost;
231
- // Replace 'host' since options is not a URL object
232
- options.host = proxyHost;
233
- options.port = proxy.port;
234
- options.path = location;
235
- if (proxy.protocol) {
236
- options.protocol = proxy.protocol.includes(':') ? proxy.protocol : `${proxy.protocol}:`;
334
+ const targetIsHttps = isHttps.test(options.protocol);
335
+
336
+ if (targetIsHttps) {
337
+ // CONNECT-tunneling path for HTTPS targets. Preserves end-to-end TLS to
338
+ // the origin so the proxy cannot inspect the URL, headers, or body — the
339
+ // behavior already promised by THREATMODEL.md (T-R9). HttpsProxyAgent
340
+ // sends Proxy-Authorization on the CONNECT request only, never on the
341
+ // wrapped TLS request, which is why we don't stamp it onto
342
+ // options.headers here. If the user already supplied an HttpsProxyAgent,
343
+ // they own tunneling end-to-end and we leave them alone; otherwise we
344
+ // install our own tunneling agent and forward their TLS options (if any)
345
+ // so a custom httpsAgent for cert pinning / rejectUnauthorized still
346
+ // applies to the origin TLS upgrade.
347
+ if (!(configHttpsAgent instanceof HttpsProxyAgent)) {
348
+ const proxyHost = readProxyField('hostname') || readProxyField('host');
349
+ const proxyPort = readProxyField('port');
350
+ const rawProxyProtocol = readProxyField('protocol');
351
+ const normalizedProtocol = rawProxyProtocol
352
+ ? rawProxyProtocol.includes(':')
353
+ ? rawProxyProtocol
354
+ : `${rawProxyProtocol}:`
355
+ : 'http:';
356
+ // Bracket IPv6 literals for URL parsing; URL.hostname strips the
357
+ // brackets again on read so the agent receives the raw form.
358
+ const proxyHostForURL =
359
+ proxyHost && proxyHost.includes(':') && !proxyHost.startsWith('[')
360
+ ? `[${proxyHost}]`
361
+ : proxyHost;
362
+ const proxyURL = new URL(
363
+ `${normalizedProtocol}//${proxyHostForURL}${proxyPort ? ':' + proxyPort : ''}`
364
+ );
365
+ const agentOptions = {
366
+ protocol: proxyURL.protocol,
367
+ hostname: proxyURL.hostname.replace(/^\[|\]$/g, ''),
368
+ port: proxyURL.port,
369
+ auth: proxyAuth && typeof proxyAuth === 'string' ? proxyAuth : undefined,
370
+ };
371
+ if (proxyURL.protocol === 'https:') {
372
+ agentOptions.ALPNProtocols = ['http/1.1'];
373
+ }
374
+ const tunnelingAgent = getTunnelingAgent(agentOptions, configHttpsAgent);
375
+ // Set both: `options.agent` is consumed by the native https.request path
376
+ // (config.maxRedirects === 0); `options.agents.https` is consumed by
377
+ // follow-redirects, which ignores `options.agent` when `options.agents`
378
+ // is present.
379
+ options.agent = tunnelingAgent;
380
+ if (options.agents) {
381
+ options.agents.https = tunnelingAgent;
382
+ }
383
+ }
384
+ } else {
385
+ // Forward-proxy mode for plaintext HTTP targets. The request line carries
386
+ // the absolute URL and the proxy sees everything — acceptable for plain
387
+ // HTTP since the wire was already plaintext.
388
+ if (proxyAuth) {
389
+ const base64 = Buffer.from(proxyAuth, 'utf8').toString('base64');
390
+ options.headers['Proxy-Authorization'] = 'Basic ' + base64;
391
+ }
392
+
393
+ // Preserve a user-supplied Host header (case-insensitive) so callers can override
394
+ // the value forwarded to the proxy; otherwise default to the request URL's host.
395
+ let hasUserHostHeader = false;
396
+ for (const name of Object.keys(options.headers)) {
397
+ if (name.toLowerCase() === 'host') {
398
+ hasUserHostHeader = true;
399
+ break;
400
+ }
401
+ }
402
+ if (!hasUserHostHeader) {
403
+ options.headers.host = options.hostname + (options.port ? ':' + options.port : '');
404
+ }
405
+ const proxyHost = readProxyField('hostname') || readProxyField('host');
406
+ options.hostname = proxyHost;
407
+ // Replace 'host' since options is not a URL object
408
+ options.host = proxyHost;
409
+ options.port = readProxyField('port');
410
+ options.path = location;
411
+ const proxyProtocol = readProxyField('protocol');
412
+ if (proxyProtocol) {
413
+ options.protocol = proxyProtocol.includes(':') ? proxyProtocol : `${proxyProtocol}:`;
414
+ }
237
415
  }
238
416
  }
239
417
 
240
418
  options.beforeRedirects.proxy = function beforeRedirect(redirectOptions) {
241
419
  // Configure proxy for redirected request, passing the original config proxy to apply
242
420
  // the exact same logic as if the redirected request was performed by axios directly.
243
- setProxy(redirectOptions, configProxy, redirectOptions.href);
421
+ setProxy(redirectOptions, configProxy, redirectOptions.href, true, configHttpsAgent);
244
422
  };
245
423
  }
246
424
 
@@ -352,6 +530,7 @@ export default isHttpAdapterSupported &&
352
530
  let isDone;
353
531
  let rejected = false;
354
532
  let req;
533
+ let connectPhaseTimer;
355
534
 
356
535
  httpVersion = +httpVersion;
357
536
 
@@ -396,9 +575,34 @@ export default isHttpAdapterSupported &&
396
575
  }
397
576
  }
398
577
 
578
+ function clearConnectPhaseTimer() {
579
+ if (connectPhaseTimer) {
580
+ clearTimeout(connectPhaseTimer);
581
+ connectPhaseTimer = null;
582
+ }
583
+ }
584
+
585
+ function createTimeoutError() {
586
+ let timeoutErrorMessage = config.timeout
587
+ ? 'timeout of ' + config.timeout + 'ms exceeded'
588
+ : 'timeout exceeded';
589
+ const transitional = config.transitional || transitionalDefaults;
590
+ if (config.timeoutErrorMessage) {
591
+ timeoutErrorMessage = config.timeoutErrorMessage;
592
+ }
593
+ return new AxiosError(
594
+ timeoutErrorMessage,
595
+ transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED,
596
+ config,
597
+ req
598
+ );
599
+ }
600
+
399
601
  abortEmitter.once('abort', reject);
400
602
 
401
603
  const onFinished = () => {
604
+ clearConnectPhaseTimer();
605
+
402
606
  if (config.cancelToken) {
403
607
  config.cancelToken.unsubscribe(abort);
404
608
  }
@@ -419,6 +623,7 @@ export default isHttpAdapterSupported &&
419
623
 
420
624
  onDone((response, isRejected) => {
421
625
  isDone = true;
626
+ clearConnectPhaseTimer();
422
627
 
423
628
  if (isRejected) {
424
629
  rejected = true;
@@ -533,9 +738,12 @@ export default isHttpAdapterSupported &&
533
738
  }
534
739
  );
535
740
  // support for https://www.npmjs.com/package/form-data api
536
- } else if (utils.isFormData(data) && utils.isFunction(data.getHeaders) &&
537
- data.getHeaders !== Object.prototype.getHeaders) {
538
- headers.set(data.getHeaders());
741
+ } else if (
742
+ utils.isFormData(data) &&
743
+ utils.isFunction(data.getHeaders) &&
744
+ data.getHeaders !== Object.prototype.getHeaders
745
+ ) {
746
+ setFormDataHeaders(headers, data.getHeaders(), own('formDataHeaderPolicy'));
539
747
 
540
748
  if (!headers.hasContentLength()) {
541
749
  try {
@@ -628,8 +836,8 @@ export default isHttpAdapterSupported &&
628
836
  }
629
837
 
630
838
  if (!auth && parsed.username) {
631
- const urlUsername = parsed.username;
632
- const urlPassword = parsed.password;
839
+ const urlUsername = decodeURIComponentSafe(parsed.username);
840
+ const urlPassword = decodeURIComponentSafe(parsed.password);
633
841
  auth = urlUsername + ':' + urlPassword;
634
842
  }
635
843
 
@@ -659,11 +867,10 @@ export default isHttpAdapterSupported &&
659
867
 
660
868
  // Null-prototype to block prototype pollution gadgets on properties read
661
869
  // directly by Node's http.request (e.g. insecureHTTPParser, lookup).
662
- // See GHSA-q8qp-cvcw-x6jj.
663
870
  const options = Object.assign(Object.create(null), {
664
871
  path,
665
872
  method: method,
666
- headers: headers.toJSON(),
873
+ headers: toByteStringHeaderObject(headers),
667
874
  agents: { http: config.httpAgent, https: config.httpsAgent },
668
875
  auth,
669
876
  protocol,
@@ -678,11 +885,9 @@ export default isHttpAdapterSupported &&
678
885
 
679
886
  if (config.socketPath) {
680
887
  if (typeof config.socketPath !== 'string') {
681
- return reject(new AxiosError(
682
- 'socketPath must be a string',
683
- AxiosError.ERR_BAD_OPTION_VALUE,
684
- config
685
- ));
888
+ return reject(
889
+ new AxiosError('socketPath must be a string', AxiosError.ERR_BAD_OPTION_VALUE, config)
890
+ );
686
891
  }
687
892
 
688
893
  if (config.allowedSocketPaths != null) {
@@ -696,11 +901,13 @@ export default isHttpAdapterSupported &&
696
901
  );
697
902
 
698
903
  if (!isAllowed) {
699
- return reject(new AxiosError(
700
- `socketPath "${config.socketPath}" is not permitted by allowedSocketPaths`,
701
- AxiosError.ERR_BAD_OPTION_VALUE,
702
- config
703
- ));
904
+ return reject(
905
+ new AxiosError(
906
+ `socketPath "${config.socketPath}" is not permitted by allowedSocketPaths`,
907
+ AxiosError.ERR_BAD_OPTION_VALUE,
908
+ config
909
+ )
910
+ );
704
911
  }
705
912
  }
706
913
 
@@ -713,12 +920,19 @@ export default isHttpAdapterSupported &&
713
920
  setProxy(
714
921
  options,
715
922
  config.proxy,
716
- protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path
923
+ protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path,
924
+ false,
925
+ config.httpsAgent
717
926
  );
718
927
  }
719
928
  let transport;
929
+ let isNativeTransport = false;
720
930
  const isHttpsRequest = isHttps.test(options.protocol);
721
- options.agent = isHttpsRequest ? config.httpsAgent : config.httpAgent;
931
+ // Don't clobber a CONNECT-tunneling agent installed by setProxy() for an
932
+ // HTTPS target.
933
+ if (options.agent == null) {
934
+ options.agent = isHttpsRequest ? config.httpsAgent : config.httpAgent;
935
+ }
722
936
 
723
937
  if (isHttp2) {
724
938
  transport = http2Transport;
@@ -728,6 +942,7 @@ export default isHttpAdapterSupported &&
728
942
  transport = configTransport;
729
943
  } else if (config.maxRedirects === 0) {
730
944
  transport = isHttpsRequest ? https : http;
945
+ isNativeTransport = true;
731
946
  } else {
732
947
  if (config.maxRedirects) {
733
948
  options.maxRedirects = config.maxRedirects;
@@ -749,11 +964,13 @@ export default isHttpAdapterSupported &&
749
964
 
750
965
  // Always set an explicit own value so a polluted
751
966
  // Object.prototype.insecureHTTPParser cannot enable the lenient parser
752
- // through Node's internal options copy (GHSA-q8qp-cvcw-x6jj).
967
+ // through Node's internal options copy
753
968
  options.insecureHTTPParser = Boolean(own('insecureHTTPParser'));
754
969
 
755
970
  // Create the request
756
971
  req = transport.request(options, function handleResponse(res) {
972
+ clearConnectPhaseTimer();
973
+
757
974
  if (req.destroyed) return;
758
975
 
759
976
  const streams = [res];
@@ -835,7 +1052,7 @@ export default isHttpAdapterSupported &&
835
1052
 
836
1053
  if (responseType === 'stream') {
837
1054
  // Enforce maxContentLength on streamed responses; previously this
838
- // was applied only to buffered responses. See GHSA-vf2m-468p-8v99.
1055
+ // was applied only to buffered responses.
839
1056
  if (config.maxContentLength > -1) {
840
1057
  const limit = config.maxContentLength;
841
1058
  const source = responseStream;
@@ -893,15 +1110,16 @@ export default isHttpAdapterSupported &&
893
1110
  'stream has been aborted',
894
1111
  AxiosError.ERR_BAD_RESPONSE,
895
1112
  config,
896
- lastRequest
1113
+ lastRequest,
1114
+ response
897
1115
  );
898
1116
  responseStream.destroy(err);
899
1117
  reject(err);
900
1118
  });
901
1119
 
902
1120
  responseStream.on('error', function handleStreamError(err) {
903
- if (req.destroyed) return;
904
- reject(AxiosError.from(err, null, config, lastRequest));
1121
+ if (rejected) return;
1122
+ reject(AxiosError.from(err, null, config, lastRequest, response));
905
1123
  });
906
1124
 
907
1125
  responseStream.on('end', function handleStreamEnd() {
@@ -944,6 +1162,16 @@ export default isHttpAdapterSupported &&
944
1162
  });
945
1163
 
946
1164
  // set tcp keep alive to prevent drop connection by peer
1165
+ // Track every socket bound to this outer RedirectableRequest so a single
1166
+ // 'close' listener can release ownership on all of them. follow-redirects
1167
+ // re-emits the 'socket' event for each hop's native request onto the same
1168
+ // outer request, so attaching per-request listeners inside this handler
1169
+ // would accumulate across hops and trigger MaxListenersExceededWarning at
1170
+ // >= 11 redirects. Clearing only the last-bound socket would leave stale
1171
+ // kAxiosCurrentReq refs on earlier hop sockets returned to the keep-alive
1172
+ // pool, causing an idle-pool 'error' to be attributed to a closed req.
1173
+ const boundSockets = new Set();
1174
+
947
1175
  req.on('socket', function handleRequestSocket(socket) {
948
1176
  // default interval of sending ack packet is 1 minute
949
1177
  socket.setKeepAlive(true, 1000 * 60);
@@ -964,12 +1192,18 @@ export default isHttpAdapterSupported &&
964
1192
  }
965
1193
 
966
1194
  socket[kAxiosCurrentReq] = req;
1195
+ boundSockets.add(socket);
1196
+ });
967
1197
 
968
- req.once('close', function clearCurrentReq() {
1198
+ req.once('close', function clearCurrentReq() {
1199
+ clearConnectPhaseTimer();
1200
+
1201
+ for (const socket of boundSockets) {
969
1202
  if (socket[kAxiosCurrentReq] === req) {
970
1203
  socket[kAxiosCurrentReq] = null;
971
1204
  }
972
- });
1205
+ }
1206
+ boundSockets.clear();
973
1207
  });
974
1208
 
975
1209
  // Handle request timeout
@@ -990,29 +1224,24 @@ export default isHttpAdapterSupported &&
990
1224
  return;
991
1225
  }
992
1226
 
1227
+ const handleTimeout = function handleTimeout() {
1228
+ if (isDone) return;
1229
+ abort(createTimeoutError());
1230
+ };
1231
+
1232
+ if (isNativeTransport && timeout > 0) {
1233
+ // Native ClientRequest#setTimeout starts from the socket lifecycle and
1234
+ // may not fire while TCP connect is still pending. Mirror the
1235
+ // follow-redirects wall-clock timer for the maxRedirects === 0 path.
1236
+ connectPhaseTimer = setTimeout(handleTimeout, timeout);
1237
+ }
1238
+
993
1239
  // Sometime, the response will be very slow, and does not respond, the connect event will be block by event loop system.
994
1240
  // And timer callback will be fired, and abort() will be invoked before connection, then get "socket hang up" and code ECONNRESET.
995
1241
  // At this time, if we have a large number of request, nodejs will hang up some socket on background. and the number will up and up.
996
1242
  // And then these socket which be hang up will devouring CPU little by little.
997
1243
  // ClientRequest.setTimeout will be fired on the specify milliseconds, and can make sure that abort() will be fired after connect.
998
- req.setTimeout(timeout, function handleRequestTimeout() {
999
- if (isDone) return;
1000
- let timeoutErrorMessage = config.timeout
1001
- ? 'timeout of ' + config.timeout + 'ms exceeded'
1002
- : 'timeout exceeded';
1003
- const transitional = config.transitional || transitionalDefaults;
1004
- if (config.timeoutErrorMessage) {
1005
- timeoutErrorMessage = config.timeoutErrorMessage;
1006
- }
1007
- abort(
1008
- new AxiosError(
1009
- timeoutErrorMessage,
1010
- transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED,
1011
- config,
1012
- req
1013
- )
1014
- );
1015
- });
1244
+ req.setTimeout(timeout, handleTimeout);
1016
1245
  } else {
1017
1246
  // explicitly reset the socket timeout value for a possible `keep-alive` request
1018
1247
  req.setTimeout(0);
@@ -1040,7 +1269,7 @@ export default isHttpAdapterSupported &&
1040
1269
 
1041
1270
  // Enforce maxBodyLength for streamed uploads on the native http/https
1042
1271
  // transport (maxRedirects === 0); follow-redirects enforces it on the
1043
- // other path. See GHSA-5c9x-8gcm-mpgx.
1272
+ // other path.
1044
1273
  let uploadStream = data;
1045
1274
  if (config.maxBodyLength > -1 && config.maxRedirects === 0) {
1046
1275
  const limit = config.maxBodyLength;
@@ -8,6 +8,7 @@ import platform from '../platform/index.js';
8
8
  import AxiosHeaders from '../core/AxiosHeaders.js';
9
9
  import { progressEventReducer } from '../helpers/progressEventReducer.js';
10
10
  import resolveConfig from '../helpers/resolveConfig.js';
11
+ import { toByteStringHeaderObject } from '../helpers/sanitizeHeaderValue.js';
11
12
 
12
13
  const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined';
13
14
 
@@ -91,7 +92,7 @@ export default isXHRAdapterSupported &&
91
92
  // will return status as 0 even though it's a successful request
92
93
  if (
93
94
  request.status === 0 &&
94
- !(request.responseURL && request.responseURL.indexOf('file:') === 0)
95
+ !(request.responseURL && request.responseURL.startsWith('file:'))
95
96
  ) {
96
97
  return;
97
98
  }
@@ -108,6 +109,7 @@ export default isXHRAdapterSupported &&
108
109
  }
109
110
 
110
111
  reject(new AxiosError('Request aborted', AxiosError.ECONNABORTED, config, request));
112
+ done();
111
113
 
112
114
  // Clean up request
113
115
  request = null;
@@ -123,6 +125,7 @@ export default isXHRAdapterSupported &&
123
125
  // attach the underlying event for consumers who want details
124
126
  err.event = event || null;
125
127
  reject(err);
128
+ done();
126
129
  request = null;
127
130
  };
128
131
 
@@ -143,6 +146,7 @@ export default isXHRAdapterSupported &&
143
146
  request
144
147
  )
145
148
  );
149
+ done();
146
150
 
147
151
  // Clean up request
148
152
  request = null;
@@ -153,7 +157,7 @@ export default isXHRAdapterSupported &&
153
157
 
154
158
  // Add headers to the request
155
159
  if ('setRequestHeader' in request) {
156
- utils.forEach(requestHeaders.toJSON(), function setRequestHeader(val, key) {
160
+ utils.forEach(toByteStringHeaderObject(requestHeaders), function setRequestHeader(val, key) {
157
161
  request.setRequestHeader(key, val);
158
162
  });
159
163
  }
@@ -192,6 +196,7 @@ export default isXHRAdapterSupported &&
192
196
  }
193
197
  reject(!cancel || cancel.type ? new CanceledError(null, config, request) : cancel);
194
198
  request.abort();
199
+ done();
195
200
  request = null;
196
201
  };
197
202
 
@@ -205,7 +210,7 @@ export default isXHRAdapterSupported &&
205
210
 
206
211
  const protocol = parseProtocol(_config.url);
207
212
 
208
- if (protocol && platform.protocols.indexOf(protocol) === -1) {
213
+ if (protocol && !platform.protocols.includes(protocol)) {
209
214
  reject(
210
215
  new AxiosError(
211
216
  'Unsupported protocol ' + protocol + ':',
package/lib/core/Axios.js CHANGED
@@ -148,7 +148,7 @@ class Axios {
148
148
  let contextHeaders = headers && utils.merge(headers.common, headers[config.method]);
149
149
 
150
150
  headers &&
151
- utils.forEach(['delete', 'get', 'head', 'post', 'put', 'patch', 'common'], (method) => {
151
+ utils.forEach(['delete', 'get', 'head', 'post', 'put', 'patch', 'query', 'common'], (method) => {
152
152
  delete headers[method];
153
153
  });
154
154
 
@@ -251,7 +251,7 @@ utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData
251
251
  };
252
252
  });
253
253
 
254
- utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
254
+ utils.forEach(['post', 'put', 'patch', 'query'], function forEachMethodWithData(method) {
255
255
  function generateHTTPMethod(isForm) {
256
256
  return function httpMethod(url, data, config) {
257
257
  return this.request(
@@ -271,7 +271,11 @@ utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
271
271
 
272
272
  Axios.prototype[method] = generateHTTPMethod();
273
273
 
274
- Axios.prototype[method + 'Form'] = generateHTTPMethod(true);
274
+ // QUERY is a safe/idempotent read method; multipart form bodies don't fit
275
+ // its semantics, so no queryForm shorthand is generated.
276
+ if (method !== 'query') {
277
+ Axios.prototype[method + 'Form'] = generateHTTPMethod(true);
278
+ }
275
279
  });
276
280
 
277
281
  export default Axios;