axios 1.18.0 → 1.19.0

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 (41) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +192 -23
  3. package/dist/axios.js +410 -101
  4. package/dist/axios.min.js +3 -3
  5. package/dist/axios.min.js.map +1 -1
  6. package/dist/browser/axios.cjs +484 -119
  7. package/dist/esm/axios.js +484 -119
  8. package/dist/esm/axios.min.js +2 -2
  9. package/dist/esm/axios.min.js.map +1 -1
  10. package/dist/node/axios.cjs +589 -139
  11. package/index.d.cts +114 -74
  12. package/index.d.ts +112 -68
  13. package/lib/adapters/adapters.js +1 -1
  14. package/lib/adapters/fetch.js +27 -12
  15. package/lib/adapters/http.js +95 -34
  16. package/lib/adapters/xhr.js +1 -0
  17. package/lib/core/Axios.js +24 -6
  18. package/lib/core/AxiosError.js +46 -3
  19. package/lib/core/AxiosHeaders.js +124 -1
  20. package/lib/core/buildFullPath.js +45 -7
  21. package/lib/core/mergeConfig.js +19 -3
  22. package/lib/core/setFormDataHeaders.js +27 -0
  23. package/lib/env/data.js +1 -1
  24. package/lib/helpers/AxiosURLSearchParams.js +1 -3
  25. package/lib/helpers/HttpStatusCode.js +1 -0
  26. package/lib/helpers/buildURL.js +1 -0
  27. package/lib/helpers/combineURLs.js +11 -3
  28. package/lib/helpers/composeSignals.js +12 -1
  29. package/lib/helpers/cookies.js +5 -1
  30. package/lib/helpers/estimateDataURLDecodedBytes.js +115 -54
  31. package/lib/helpers/formDataToJSON.js +11 -5
  32. package/lib/helpers/fromDataURI.js +4 -2
  33. package/lib/helpers/parseHeaders.js +5 -3
  34. package/lib/helpers/progressEventReducer.js +3 -3
  35. package/lib/helpers/resolveConfig.js +10 -19
  36. package/lib/helpers/shouldBypassProxy.js +120 -1
  37. package/lib/helpers/toFormData.js +8 -1
  38. package/lib/helpers/validator.js +1 -1
  39. package/lib/platform/node/classes/Buffer.js +11 -0
  40. package/lib/utils.js +17 -5
  41. package/package.json +22 -20
@@ -218,6 +218,7 @@ export default isXHRAdapterSupported &&
218
218
  config
219
219
  )
220
220
  );
221
+ done();
221
222
  return;
222
223
  }
223
224
 
package/lib/core/Axios.js CHANGED
@@ -209,17 +209,35 @@ class Axios {
209
209
  const onFulfilled = requestInterceptorChain[i++];
210
210
  const onRejected = requestInterceptorChain[i++];
211
211
  try {
212
- newConfig = onFulfilled(newConfig);
212
+ newConfig = onFulfilled ? onFulfilled(newConfig) : newConfig;
213
213
  } catch (error) {
214
- onRejected.call(this, error);
214
+ if (!onRejected) {
215
+ promise = Promise.reject(error);
216
+ break;
217
+ }
218
+
219
+ try {
220
+ const rejectedResult = onRejected.call(this, error);
221
+
222
+ if (utils.isThenable(rejectedResult)) {
223
+ promise = Promise.resolve(rejectedResult).then(() =>
224
+ dispatchRequest.call(this, newConfig)
225
+ );
226
+ }
227
+ } catch (rejectedError) {
228
+ promise = Promise.reject(rejectedError);
229
+ }
230
+
215
231
  break;
216
232
  }
217
233
  }
218
234
 
219
- try {
220
- promise = dispatchRequest.call(this, newConfig);
221
- } catch (error) {
222
- return Promise.reject(error);
235
+ if (!promise) {
236
+ try {
237
+ promise = dispatchRequest.call(this, newConfig);
238
+ } catch (error) {
239
+ promise = Promise.reject(error);
240
+ }
223
241
  }
224
242
 
225
243
  i = 0;
@@ -3,7 +3,7 @@
3
3
  import utils from '../utils.js';
4
4
  import AxiosHeaders from './AxiosHeaders.js';
5
5
 
6
- const REDACTED = '[REDACTED ****]';
6
+ export const REDACTED = '[REDACTED ****]';
7
7
 
8
8
  function hasOwnOrPrototypeToJSON(source) {
9
9
  if (utils.hasOwnProp(source, 'toJSON')) {
@@ -72,10 +72,53 @@ function redactConfig(config, redactKeys) {
72
72
  return visit(config);
73
73
  }
74
74
 
75
+ function stringifySafely(value) {
76
+ try {
77
+ return String(value);
78
+ } catch (err) {
79
+ return '';
80
+ }
81
+ }
82
+
83
+ function aggregateErrorMessage(error) {
84
+ const message = error.errors
85
+ .map((entry) => {
86
+ try {
87
+ return entry && entry.message ? stringifySafely(entry.message) : stringifySafely(entry);
88
+ } catch (err) {
89
+ return '';
90
+ }
91
+ })
92
+ .filter(Boolean)
93
+ .join('; ');
94
+
95
+ return message || error.name || 'AggregateError';
96
+ }
97
+
75
98
  class AxiosError extends Error {
76
99
  static from(error, code, config, request, response, customProps) {
77
- const axiosError = new AxiosError(error.message, code || error.code, config, request, response);
78
- axiosError.cause = error;
100
+ // `AggregateError` (thrown by Node on dual-stack/Happy-Eyeballs connection
101
+ // failures) has an empty `message`; its detail lives in `errors[]`. Without
102
+ // this, the wrapped error surfaces with a blank message (see #6721).
103
+ let message = error.message;
104
+ if (!message && utils.isArray(error.errors) && error.errors.length) {
105
+ message = aggregateErrorMessage(error);
106
+ }
107
+
108
+ const axiosError = new AxiosError(message, code || error.code, config, request, response);
109
+ // Match native `Error` `cause` semantics: non-enumerable. The wrapped
110
+ // error often carries circular internals (sockets, requests, agents), so
111
+ // an enumerable `cause` makes structured loggers (pino/winston) and any
112
+ // own-property walk throw "Converting circular structure to JSON".
113
+ // Regression from #6982; see #7205. `__proto__: null` mirrors the
114
+ // `message` descriptor below (prototype-pollution-safe descriptor).
115
+ Object.defineProperty(axiosError, 'cause', {
116
+ __proto__: null,
117
+ value: error,
118
+ writable: true,
119
+ enumerable: false,
120
+ configurable: true,
121
+ });
79
122
  axiosError.name = error.name;
80
123
 
81
124
  // Preserve status from the original error if not already set from response
@@ -30,6 +30,124 @@ function parseTokens(str) {
30
30
  return tokens;
31
31
  }
32
32
 
33
+ const parameterNameRE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
34
+
35
+ function trimOWS(value) {
36
+ let start = 0;
37
+ let end = value.length;
38
+
39
+ while (start < end) {
40
+ const code = value.charCodeAt(start);
41
+
42
+ if (code !== 0x09 && code !== 0x20) {
43
+ break;
44
+ }
45
+
46
+ start += 1;
47
+ }
48
+
49
+ while (end > start) {
50
+ const code = value.charCodeAt(end - 1);
51
+
52
+ if (code !== 0x09 && code !== 0x20) {
53
+ break;
54
+ }
55
+
56
+ end -= 1;
57
+ }
58
+
59
+ return start === 0 && end === value.length ? value : value.slice(start, end);
60
+ }
61
+
62
+ function decodeQuotedString(value) {
63
+ const last = value.length - 1;
64
+
65
+ if (last < 1 || value.charCodeAt(0) !== 0x22 || value.charCodeAt(last) !== 0x22) {
66
+ return value;
67
+ }
68
+
69
+ let decoded = '';
70
+
71
+ for (let i = 1; i < last; i++) {
72
+ const code = value.charCodeAt(i);
73
+
74
+ if (code === 0x22) {
75
+ return value;
76
+ }
77
+
78
+ if (code === 0x5c) {
79
+ i += 1;
80
+
81
+ if (i >= last) {
82
+ return value;
83
+ }
84
+ }
85
+
86
+ decoded += value[i];
87
+ }
88
+
89
+ return decoded;
90
+ }
91
+
92
+ function parseParameters(value) {
93
+ const parameters = Object.create(null);
94
+ const str = String(value);
95
+ let start = 0;
96
+ let quoted = false;
97
+ let escaped = false;
98
+
99
+ function parseParameter(end) {
100
+ const part = trimOWS(str.slice(start, end));
101
+ const equals = part.indexOf('=');
102
+
103
+ if (equals < 1) {
104
+ return;
105
+ }
106
+
107
+ const name = trimOWS(part.slice(0, equals));
108
+
109
+ if (!parameterNameRE.test(name)) {
110
+ return;
111
+ }
112
+
113
+ const normalizedName = name.toLowerCase();
114
+
115
+ if (
116
+ normalizedName === '__proto__' ||
117
+ normalizedName === 'constructor' ||
118
+ normalizedName === 'prototype'
119
+ ) {
120
+ return;
121
+ }
122
+
123
+ const parameterValue = trimOWS(part.slice(equals + 1));
124
+ parameters[normalizedName] = decodeQuotedString(parameterValue);
125
+ }
126
+
127
+ for (let i = 0; i < str.length; i++) {
128
+ const code = str.charCodeAt(i);
129
+
130
+ if (quoted) {
131
+ if (escaped) {
132
+ escaped = false;
133
+ } else if (code === 0x5c) {
134
+ escaped = true;
135
+ } else if (code === 0x22) {
136
+ quoted = false;
137
+ }
138
+ } else if (code === 0x22) {
139
+ quoted = true;
140
+ } else if (code === 0x2c || code === 0x3b) {
141
+ parseParameter(i);
142
+ start = i + 1;
143
+ }
144
+ }
145
+
146
+ parseParameter(str.length);
147
+
148
+ return parameters;
149
+ }
150
+
33
151
  const isValidHeaderName = (str) => /^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(str.trim());
34
152
 
35
153
  function matchHeaderValue(context, value, header, filter, isHeaderNameFilter) {
@@ -281,7 +399,8 @@ class AxiosHeaders {
281
399
  }
282
400
 
283
401
  getSetCookie() {
284
- return this.get('set-cookie') || [];
402
+ const value = this.get('set-cookie');
403
+ return utils.isArray(value) ? value : value == null || value === false ? [] : [value];
285
404
  }
286
405
 
287
406
  get [Symbol.toStringTag]() {
@@ -292,6 +411,10 @@ class AxiosHeaders {
292
411
  return thing instanceof this ? thing : new this(thing);
293
412
  }
294
413
 
414
+ static parseParameters(value) {
415
+ return parseParameters(value);
416
+ }
417
+
295
418
  static concat(first, ...targets) {
296
419
  const computed = new this(first);
297
420
 
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- import AxiosError from './AxiosError.js';
3
+ import AxiosError, { REDACTED } from './AxiosError.js';
4
4
  import isAbsoluteURL from '../helpers/isAbsoluteURL.js';
5
5
  import combineURLs from '../helpers/combineURLs.js';
6
6
 
@@ -19,13 +19,51 @@ function normalizeURLForProtocolCheck(url) {
19
19
  return stripLeadingC0ControlOrSpace(url).replace(httpProtocolControlCharacters, '');
20
20
  }
21
21
 
22
+ // Redact the parts of a URL that can carry secrets before it is embedded in an
23
+ // error message. AxiosError.toJSON() serializes `message` verbatim and errors
24
+ // are commonly logged, while the opt-in `config.redact` model only cleans
25
+ // config keys — it cannot reach the message. Redact only the genuinely
26
+ // sensitive substrings — userinfo (credentials), query parameter values and
27
+ // fragment contents — with the same REDACTED marker the config redaction uses,
28
+ // while keeping the scheme, host, path and parameter names so the offending
29
+ // request stays accurately identifiable.
30
+ function redactFragment(fragment) {
31
+ if (!fragment) {
32
+ return fragment;
33
+ }
34
+
35
+ return fragment.replace(/(^|&)([^=&]*=)?[^&]+/g, (match, separator, parameterName = '') => {
36
+ return `${separator}${parameterName}${REDACTED}`;
37
+ });
38
+ }
39
+
40
+ function redactSensitiveURLParts(url) {
41
+ const redactedURL = url.replace(/^(https?:\/{0,2})[^/?#]*@/i, `$1${REDACTED}@`);
42
+ const fragmentIndex = redactedURL.indexOf('#');
43
+ const urlWithoutFragment =
44
+ fragmentIndex === -1 ? redactedURL : redactedURL.slice(0, fragmentIndex);
45
+ const redactedURLWithoutFragment = urlWithoutFragment.replace(
46
+ /([?&][^=&#]*=)[^&#]*/g,
47
+ `$1${REDACTED}`
48
+ );
49
+
50
+ if (fragmentIndex === -1) {
51
+ return redactedURLWithoutFragment;
52
+ }
53
+
54
+ return `${redactedURLWithoutFragment}#${redactFragment(redactedURL.slice(fragmentIndex + 1))}`;
55
+ }
56
+
22
57
  function assertValidHttpProtocolURL(url, config) {
23
- if (typeof url === 'string' && malformedHttpProtocol.test(normalizeURLForProtocolCheck(url))) {
24
- throw new AxiosError(
25
- 'Invalid URL: missing "//" after protocol',
26
- AxiosError.ERR_INVALID_URL,
27
- config
28
- );
58
+ if (typeof url === 'string') {
59
+ const normalizedURL = normalizeURLForProtocolCheck(url);
60
+ if (malformedHttpProtocol.test(normalizedURL)) {
61
+ throw new AxiosError(
62
+ `Invalid URL ${JSON.stringify(redactSensitiveURLParts(normalizedURL))}: missing "//" after protocol`,
63
+ AxiosError.ERR_INVALID_URL,
64
+ config
65
+ );
66
+ }
29
67
  }
30
68
  }
31
69
 
@@ -5,6 +5,17 @@ import AxiosHeaders from './AxiosHeaders.js';
5
5
 
6
6
  const headersToObject = (thing) => (thing instanceof AxiosHeaders ? { ...thing } : thing);
7
7
 
8
+ const ownEnumerableKeys = (thing) => {
9
+ if (Object.getOwnPropertySymbols && Object.getOwnPropertyDescriptor) {
10
+ return Object.keys(thing).concat(
11
+ Object.getOwnPropertySymbols(thing).filter(
12
+ (symbol) => Object.getOwnPropertyDescriptor(thing, symbol).enumerable
13
+ )
14
+ );
15
+ }
16
+ return Object.keys(thing);
17
+ };
18
+
8
19
  /**
9
20
  * Config-specific merge-function which creates a new config-object
10
21
  * by merging two configuration objects together.
@@ -16,6 +27,7 @@ const headersToObject = (thing) => (thing instanceof AxiosHeaders ? { ...thing }
16
27
  */
17
28
  export default function mergeConfig(config1, config2) {
18
29
  // eslint-disable-next-line no-param-reassign
30
+ config1 = config1 || {};
19
31
  config2 = config2 || {};
20
32
 
21
33
  // Use a null-prototype object so that downstream reads such as `config.auth`
@@ -69,7 +81,9 @@ export default function mergeConfig(config1, config2) {
69
81
  }
70
82
 
71
83
  function getMergedTransitionalOption(prop) {
72
- const transitional2 = utils.hasOwnProp(config2, 'transitional') ? config2.transitional : undefined;
84
+ const transitional2 = utils.hasOwnProp(config2, 'transitional')
85
+ ? config2.transitional
86
+ : undefined;
73
87
 
74
88
  if (!utils.isUndefined(transitional2)) {
75
89
  if (utils.isPlainObject(transitional2)) {
@@ -81,7 +95,9 @@ export default function mergeConfig(config1, config2) {
81
95
  }
82
96
  }
83
97
 
84
- const transitional1 = utils.hasOwnProp(config1, 'transitional') ? config1.transitional : undefined;
98
+ const transitional1 = utils.hasOwnProp(config1, 'transitional')
99
+ ? config1.transitional
100
+ : undefined;
85
101
 
86
102
  if (utils.isPlainObject(transitional1) && utils.hasOwnProp(transitional1, prop)) {
87
103
  return transitional1[prop];
@@ -133,7 +149,7 @@ export default function mergeConfig(config1, config2) {
133
149
  mergeDeepProperties(headersToObject(a), headersToObject(b), prop, true),
134
150
  };
135
151
 
136
- utils.forEach(Object.keys({ ...config1, ...config2 }), function computeConfigValue(prop) {
152
+ utils.forEach(ownEnumerableKeys({ ...config1, ...config2 }), function computeConfigValue(prop) {
137
153
  if (prop === '__proto__' || prop === 'constructor' || prop === 'prototype') return;
138
154
  const merge = utils.hasOwnProp(mergeMap, prop) ? mergeMap[prop] : mergeDeepProperties;
139
155
  const a = utils.hasOwnProp(config1, prop) ? config1[prop] : undefined;
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+
3
+ const FORM_DATA_CONTENT_HEADERS = ['content-type', 'content-length'];
4
+
5
+ /**
6
+ * Apply the headers generated by a FormData implementation to the request headers,
7
+ * honoring the `formDataHeaderPolicy` option: with 'content-only', copy only the
8
+ * content-* headers; otherwise merge all of them.
9
+ *
10
+ * @param {AxiosHeaders} headers - the request headers to mutate
11
+ * @param {Object | null | undefined} formHeaders - headers produced by the FormData implementation
12
+ * @param {String} [policy] - the resolved `formDataHeaderPolicy` config value
13
+ *
14
+ * @returns {void}
15
+ */
16
+ export default function setFormDataHeaders(headers, formHeaders, policy) {
17
+ if (policy !== 'content-only') {
18
+ headers.set(formHeaders);
19
+ return;
20
+ }
21
+
22
+ Object.entries(formHeaders || {}).forEach(([key, val]) => {
23
+ if (FORM_DATA_CONTENT_HEADERS.includes(key.toLowerCase())) {
24
+ headers.set(key, val);
25
+ }
26
+ });
27
+ }
package/lib/env/data.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "1.18.0";
1
+ export const VERSION = "1.19.0";
@@ -46,9 +46,7 @@ prototype.append = function append(name, value) {
46
46
 
47
47
  prototype.toString = function toString(encoder) {
48
48
  const _encode = encoder
49
- ? function (value) {
50
- return encoder.call(this, value, encode);
51
- }
49
+ ? (value) => encoder.call(this, value, encode)
52
50
  : encode;
53
51
 
54
52
  return this._pairs
@@ -62,6 +62,7 @@ const HttpStatusCode = {
62
62
  LoopDetected: 508,
63
63
  NotExtended: 510,
64
64
  NetworkAuthenticationRequired: 511,
65
+ WebServerReturnsAnUnknownError: 520,
65
66
  WebServerIsDown: 521,
66
67
  ConnectionTimedOut: 522,
67
68
  OriginIsUnreachable: 523,
@@ -32,6 +32,7 @@ export default function buildURL(url, params, options) {
32
32
  if (!params) {
33
33
  return url;
34
34
  }
35
+ url = url || '';
35
36
 
36
37
  const _options = utils.isFunction(options)
37
38
  ? {
@@ -9,7 +9,15 @@
9
9
  * @returns {string} The combined URL
10
10
  */
11
11
  export default function combineURLs(baseURL, relativeURL) {
12
- return relativeURL
13
- ? baseURL.replace(/\/?\/$/, '') + '/' + relativeURL.replace(/^\/+/, '')
14
- : baseURL;
12
+ if (!relativeURL) {
13
+ return baseURL;
14
+ }
15
+
16
+ let end = baseURL.length;
17
+
18
+ while (end > 0 && baseURL.charCodeAt(end - 1) === 47) {
19
+ end--;
20
+ }
21
+
22
+ return baseURL.slice(0, end) + '/' + relativeURL.replace(/^\/+/, '');
15
23
  }
@@ -45,7 +45,18 @@ const composeSignals = (signals, timeout) => {
45
45
  signals = null;
46
46
  };
47
47
 
48
- signals.forEach((signal) => signal.addEventListener('abort', onabort));
48
+ signals.forEach((signal) => {
49
+ if (aborted) {
50
+ return;
51
+ }
52
+
53
+ if (signal.aborted) {
54
+ onabort.call(signal);
55
+ return;
56
+ }
57
+
58
+ signal.addEventListener('abort', onabort, { once: true });
59
+ });
49
60
 
50
61
  const { signal } = controller;
51
62
 
@@ -40,7 +40,11 @@ export default platform.hasStandardBrowserEnv
40
40
  const cookie = cookies[i].replace(/^\s+/, '');
41
41
  const eq = cookie.indexOf('=');
42
42
  if (eq !== -1 && cookie.slice(0, eq) === name) {
43
- return decodeURIComponent(cookie.slice(eq + 1));
43
+ try {
44
+ return decodeURIComponent(cookie.slice(eq + 1));
45
+ } catch (e) {
46
+ return cookie.slice(eq + 1);
47
+ }
44
48
  }
45
49
  }
46
50
  return null;
@@ -1,11 +1,9 @@
1
1
  /**
2
- * Estimate decoded byte length of a data:// URL *without* allocating large buffers.
3
- * - For base64: compute exact decoded size using length and padding;
4
- * handle %XX at the character-count level (no string allocation).
5
- * - For non-base64: compute the exact percent-decoded UTF-8 byte length.
6
- *
7
- * @param {string} url
8
- * @returns {number}
2
+ * Estimate data: URL byte lengths *without* allocating large buffers.
3
+ * - Fetch percent-decodes a base64 body before decoding it.
4
+ * - Node's Buffer.from(body, 'base64') sizes its backing allocation from the
5
+ * raw body, including ignored characters and content after padding.
6
+ * - Non-base64 data is percent-decoded and then encoded as UTF-8.
9
7
  */
10
8
  const isHexDigit = (charCode) =>
11
9
  (charCode >= 48 && charCode <= 57) ||
@@ -15,7 +13,89 @@ const isHexDigit = (charCode) =>
15
13
  const isPercentEncodedByte = (str, i, len) =>
16
14
  i + 2 < len && isHexDigit(str.charCodeAt(i + 1)) && isHexDigit(str.charCodeAt(i + 2));
17
15
 
18
- export default function estimateDataURLDecodedBytes(url) {
16
+ const hexValue = (charCode) => (charCode <= 57 ? charCode - 48 : (charCode & 0xdf) - 55);
17
+
18
+ const isBase64Char = (charCode) =>
19
+ (charCode >= 65 && charCode <= 90) || // A-Z
20
+ (charCode >= 97 && charCode <= 122) || // a-z
21
+ (charCode >= 48 && charCode <= 57) || // 0-9
22
+ charCode === 43 || // +
23
+ charCode === 47 || // /
24
+ charCode === 45 || // - (base64url)
25
+ charCode === 95; // _ (base64url)
26
+
27
+ const isBase64Whitespace = (charCode) =>
28
+ charCode === 9 || charCode === 10 || charCode === 12 || charCode === 13 || charCode === 32;
29
+
30
+ const base64Bytes = (significant) => {
31
+ const groups = Math.floor(significant / 4);
32
+ const remainder = significant % 4;
33
+ return groups * 3 + (remainder === 2 ? 1 : remainder === 3 ? 2 : 0);
34
+ };
35
+
36
+ // Buffer.byteLength(body, 'base64') uses the raw string length as an allocation
37
+ // upper bound even when Buffer.from later ignores characters or stops at '='.
38
+ const estimateBase64BufferAllocation = (body) => {
39
+ const len = body.length;
40
+ let padding = 0;
41
+
42
+ if (len > 0 && body.charCodeAt(len - 1) === 61 /* '=' */) {
43
+ padding++;
44
+
45
+ if (len > 1 && body.charCodeAt(len - 2) === 61 /* '=' */) {
46
+ padding++;
47
+ }
48
+ }
49
+
50
+ return Math.floor(((len - padding) * 3) / 4);
51
+ };
52
+
53
+ const estimatePercentDecodedBase64Bytes = (body) => {
54
+ const len = body.length;
55
+ let significant = 0;
56
+ let padding = 0;
57
+ let invalid = false;
58
+
59
+ for (let i = 0; i < len; i++) {
60
+ let code = body.charCodeAt(i);
61
+
62
+ if (code === 37 /* '%' */ && isPercentEncodedByte(body, i, len)) {
63
+ code = hexValue(body.charCodeAt(i + 1)) * 16 + hexValue(body.charCodeAt(i + 2));
64
+ i += 2;
65
+ }
66
+
67
+ if (isBase64Whitespace(code)) {
68
+ continue;
69
+ }
70
+
71
+ if (code === 61 /* '=' */) {
72
+ padding++;
73
+ continue;
74
+ }
75
+
76
+ if (!isBase64Char(code) || padding > 0) {
77
+ invalid = true;
78
+ continue;
79
+ }
80
+
81
+ significant++;
82
+ }
83
+
84
+ // Fetch rejects malformed forgiving-base64 input. Returning the raw-size
85
+ // allocation bound keeps that invalid input from becoming a pre-check bypass.
86
+ if (
87
+ invalid ||
88
+ padding > 2 ||
89
+ (padding > 0 && (significant + padding) % 4 !== 0) ||
90
+ significant % 4 === 1
91
+ ) {
92
+ return estimateBase64BufferAllocation(body);
93
+ }
94
+
95
+ return base64Bytes(significant);
96
+ };
97
+
98
+ const estimateDataURLBytes = (url, estimateBase64) => {
19
99
  if (!url || typeof url !== 'string') return 0;
20
100
  if (!url.startsWith('data:')) return 0;
21
101
 
@@ -27,52 +107,7 @@ export default function estimateDataURLDecodedBytes(url) {
27
107
  const isBase64 = /;base64/i.test(meta);
28
108
 
29
109
  if (isBase64) {
30
- let effectiveLen = body.length;
31
- const len = body.length; // cache length
32
-
33
- for (let i = 0; i < len; i++) {
34
- if (body.charCodeAt(i) === 37 /* '%' */ && i + 2 < len) {
35
- const a = body.charCodeAt(i + 1);
36
- const b = body.charCodeAt(i + 2);
37
- const isHex = isHexDigit(a) && isHexDigit(b);
38
-
39
- if (isHex) {
40
- effectiveLen -= 2;
41
- i += 2;
42
- }
43
- }
44
- }
45
-
46
- let pad = 0;
47
- let idx = len - 1;
48
-
49
- const tailIsPct3D = (j) =>
50
- j >= 2 &&
51
- body.charCodeAt(j - 2) === 37 && // '%'
52
- body.charCodeAt(j - 1) === 51 && // '3'
53
- (body.charCodeAt(j) === 68 || body.charCodeAt(j) === 100); // 'D' or 'd'
54
-
55
- if (idx >= 0) {
56
- if (body.charCodeAt(idx) === 61 /* '=' */) {
57
- pad++;
58
- idx--;
59
- } else if (tailIsPct3D(idx)) {
60
- pad++;
61
- idx -= 3;
62
- }
63
- }
64
-
65
- if (pad === 1 && idx >= 0) {
66
- if (body.charCodeAt(idx) === 61 /* '=' */) {
67
- pad++;
68
- } else if (tailIsPct3D(idx)) {
69
- pad++;
70
- }
71
- }
72
-
73
- const groups = Math.floor(effectiveLen / 4);
74
- const bytes = groups * 3 - (pad || 0);
75
- return bytes > 0 ? bytes : 0;
110
+ return estimateBase64(body);
76
111
  }
77
112
 
78
113
  // Compute UTF-8 byte length directly from UTF-16 code units without allocating
@@ -102,4 +137,30 @@ export default function estimateDataURLDecodedBytes(url) {
102
137
  }
103
138
  }
104
139
  return bytes;
140
+ };
141
+
142
+ /**
143
+ * Estimate the percent-decoded payload size used by Fetch data: URLs.
144
+ *
145
+ * @param {string} url
146
+ * @returns {number}
147
+ */
148
+ export default function estimateDataURLDecodedBytes(url) {
149
+ // Fetch removes URL fragments before processing a data: URL.
150
+ const fragmentIndex = typeof url === 'string' ? url.indexOf('#') : -1;
151
+
152
+ return estimateDataURLBytes(
153
+ fragmentIndex === -1 ? url : url.slice(0, fragmentIndex),
154
+ estimatePercentDecodedBase64Bytes
155
+ );
156
+ }
157
+
158
+ /**
159
+ * Estimate the Buffer backing allocation used by Node's raw base64 decoder.
160
+ *
161
+ * @param {string} url
162
+ * @returns {number}
163
+ */
164
+ export function estimateDataURLBufferAllocation(url) {
165
+ return estimateDataURLBytes(url, estimateBase64BufferAllocation);
105
166
  }