diff --git a/lib/adapters/http.js b/lib/adapters/http.js index 0577bf0b..a3489b21 100755 --- a/lib/adapters/http.js +++ b/lib/adapters/http.js @@ -25,6 +25,7 @@ import readBlob from "../helpers/readBlob.js"; import ZlibHeaderTransformStream from '../helpers/ZlibHeaderTransformStream.js'; import callbackify from "../helpers/callbackify.js"; import {progressEventReducer, progressEventDecorator, asyncDecorator} from "../helpers/progressEventReducer.js"; +import estimateDataURLDecodedBytes from '../helpers/estimateDataURLDecodedBytes.js'; const zlibOptions = { flush: zlib.constants.Z_SYNC_FLUSH, @@ -46,6 +47,7 @@ const supportedProtocols = platform.protocols.map(protocol => { return protocol + ':'; }); + const flushOnFinish = (stream, [throttled, flush]) => { stream .on('end', flush) @@ -54,6 +56,7 @@ const flushOnFinish = (stream, [throttled, flush]) => { return throttled; } + /** * If the proxy or config beforeRedirects functions are defined, call them with the options * object. @@ -233,6 +236,21 @@ export default isHttpAdapterSupported && function httpAdapter(config) { const protocol = parsed.protocol || supportedProtocols[0]; if (protocol === 'data:') { + // Apply the same semantics as HTTP: only enforce if a finite, non-negative cap is set. + if (config.maxContentLength > -1) { + // Use the exact string passed to fromDataURI (config.url); fall back to fullPath if needed. + const dataUrl = String(config.url || fullPath || ''); + const estimated = estimateDataURLDecodedBytes(dataUrl); + + if (estimated > config.maxContentLength) { + return reject(new AxiosError( + 'maxContentLength size of ' + config.maxContentLength + ' exceeded', + AxiosError.ERR_BAD_RESPONSE, + config + )); + } + } + let convertedData; if (method !== 'GET') { diff --git a/lib/helpers/estimateDataURLDecodedBytes.js b/lib/helpers/estimateDataURLDecodedBytes.js new file mode 100644 index 00000000..f29a8179 --- /dev/null +++ b/lib/helpers/estimateDataURLDecodedBytes.js @@ -0,0 +1,73 @@ +/** + * Estimate decoded byte length of a data:// URL *without* allocating large buffers. + * - For base64: compute exact decoded size using length and padding; + * handle %XX at the character-count level (no string allocation). + * - For non-base64: use UTF-8 byteLength of the encoded body as a safe upper bound. + * + * @param {string} url + * @returns {number} + */ +export default function estimateDataURLDecodedBytes(url) { + if (!url || typeof url !== 'string') return 0; + if (!url.startsWith('data:')) return 0; + + const comma = url.indexOf(','); + if (comma < 0) return 0; + + const meta = url.slice(5, comma); + const body = url.slice(comma + 1); + const isBase64 = /;base64/i.test(meta); + + if (isBase64) { + let effectiveLen = body.length; + const len = body.length; // cache length + + for (let i = 0; i < len; i++) { + if (body.charCodeAt(i) === 37 /* '%' */ && i + 2 < len) { + const a = body.charCodeAt(i + 1); + const b = body.charCodeAt(i + 2); + const isHex = + ((a >= 48 && a <= 57) || (a >= 65 && a <= 70) || (a >= 97 && a <= 102)) && + ((b >= 48 && b <= 57) || (b >= 65 && b <= 70) || (b >= 97 && b <= 102)); + + if (isHex) { + effectiveLen -= 2; + i += 2; + } + } + } + + let pad = 0; + let idx = len - 1; + + const tailIsPct3D = (j) => + j >= 2 && + body.charCodeAt(j - 2) === 37 && // '%' + body.charCodeAt(j - 1) === 51 && // '3' + (body.charCodeAt(j) === 68 || body.charCodeAt(j) === 100); // 'D' or 'd' + + if (idx >= 0) { + if (body.charCodeAt(idx) === 61 /* '=' */) { + pad++; + idx--; + } else if (tailIsPct3D(idx)) { + pad++; + idx -= 3; + } + } + + if (pad === 1 && idx >= 0) { + if (body.charCodeAt(idx) === 61 /* '=' */) { + pad++; + } else if (tailIsPct3D(idx)) { + pad++; + } + } + + const groups = Math.floor(effectiveLen / 4); + const bytes = groups * 3 - (pad || 0); + return bytes > 0 ? bytes : 0; + } + + return Buffer.byteLength(body, 'utf8'); +} diff --git a/lib/utils.js b/lib/utils.js index ef9ebbca..0ef93661 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -635,6 +635,8 @@ const toFiniteNumber = (value, defaultValue) => { return value != null && Number.isFinite(value = +value) ? value : defaultValue; } + + /** * If the thing is a FormData object, return true, otherwise return false. * diff --git a/test/unit/helpers/estimateDataURLDecodedBytes.spec.js b/test/unit/helpers/estimateDataURLDecodedBytes.spec.js new file mode 100644 index 00000000..8453c6ce --- /dev/null +++ b/test/unit/helpers/estimateDataURLDecodedBytes.spec.js @@ -0,0 +1,30 @@ +import assert from 'assert'; +import estimateDataURLDecodedBytes from '../../../lib/helpers/estimateDataURLDecodedBytes.js'; + +describe('estimateDataURLDecodedBytes', () => { + it('should return 0 for non-data URLs', () => { + assert.strictEqual(estimateDataURLDecodedBytes('http://example.com'), 0); + }); + + it('should calculate length for simple non-base64 data URL', () => { + const url = 'data:,Hello'; + assert.strictEqual(estimateDataURLDecodedBytes(url), Buffer.byteLength('Hello', 'utf8')); + }); + + it('should calculate decoded length for base64 data URL', () => { + const str = 'Hello'; + const b64 = Buffer.from(str, 'utf8').toString('base64'); + const url = `data:text/plain;base64,${b64}`; + assert.strictEqual(estimateDataURLDecodedBytes(url), str.length); + }); + + it('should handle base64 with = padding', () => { + const url = 'data:text/plain;base64,TQ=='; // "M" + assert.strictEqual(estimateDataURLDecodedBytes(url), 1); + }); + + it('should handle base64 with %3D padding', () => { + const url = 'data:text/plain;base64,TQ%3D%3D'; // "M" + assert.strictEqual(estimateDataURLDecodedBytes(url), 1); + }); +});