2
0
mirror of https://github.com/tenrok/axios.git synced 2026-06-23 20:40:40 +03:00

Added data URL support for node.js; (#4725)

* Added data URL support for node.js;
Added missed data URL protocol for the browser environment;
Optimized JSON parsing in the default response transformer;
Refactored project structure;
Added `cause` prop for AxiosError instance that refers to the original error if it was wrapped with `AxiosError.from` method;
Added fromDataURI helper;
Added test for handling data:url as an `arraybuffer|text|stream`;

* Added throwing of 405 HTTP error if the method is not GET;
This commit is contained in:
Dmitriy Mozgovoy
2022-05-20 09:04:36 +03:00
committed by GitHub
parent 63e559fa60
commit c30252f685
17 changed files with 226 additions and 48 deletions
+1 -1
View File
@@ -80,7 +80,7 @@ module.exports = {
'no-new-wrappers': 2, // http://eslint.org/docs/rules/no-new-wrappers 'no-new-wrappers': 2, // http://eslint.org/docs/rules/no-new-wrappers
'no-octal': 2, // http://eslint.org/docs/rules/no-octal 'no-octal': 2, // http://eslint.org/docs/rules/no-octal
'no-octal-escape': 2, // http://eslint.org/docs/rules/no-octal-escape 'no-octal-escape': 2, // http://eslint.org/docs/rules/no-octal-escape
'no-param-reassign': 2, // http://eslint.org/docs/rules/no-param-reassign 'no-param-reassign': 0, // http://eslint.org/docs/rules/no-param-reassign
'no-proto': 2, // http://eslint.org/docs/rules/no-proto 'no-proto': 2, // http://eslint.org/docs/rules/no-proto
'no-redeclare': 2, // http://eslint.org/docs/rules/no-redeclare 'no-redeclare': 2, // http://eslint.org/docs/rules/no-redeclare
'no-return-assign': 2, // http://eslint.org/docs/rules/no-return-assign 'no-return-assign': 2, // http://eslint.org/docs/rules/no-return-assign
Vendored
+3
View File
@@ -194,6 +194,7 @@ export class AxiosError<T = unknown, D = any> extends Error {
isAxiosError: boolean; isAxiosError: boolean;
status?: number; status?: number;
toJSON: () => object; toJSON: () => object;
cause?: Error;
static readonly ERR_FR_TOO_MANY_REDIRECTS = "ERR_FR_TOO_MANY_REDIRECTS"; static readonly ERR_FR_TOO_MANY_REDIRECTS = "ERR_FR_TOO_MANY_REDIRECTS";
static readonly ERR_BAD_OPTION_VALUE = "ERR_BAD_OPTION_VALUE"; static readonly ERR_BAD_OPTION_VALUE = "ERR_BAD_OPTION_VALUE";
static readonly ERR_BAD_OPTION = "ERR_BAD_OPTION"; static readonly ERR_BAD_OPTION = "ERR_BAD_OPTION";
@@ -201,6 +202,8 @@ export class AxiosError<T = unknown, D = any> extends Error {
static readonly ERR_DEPRECATED = "ERR_DEPRECATED"; static readonly ERR_DEPRECATED = "ERR_DEPRECATED";
static readonly ERR_BAD_RESPONSE = "ERR_BAD_RESPONSE"; static readonly ERR_BAD_RESPONSE = "ERR_BAD_RESPONSE";
static readonly ERR_BAD_REQUEST = "ERR_BAD_REQUEST"; static readonly ERR_BAD_REQUEST = "ERR_BAD_REQUEST";
static readonly ERR_NOT_SUPPORT = "ERR_NOT_SUPPORT";
static readonly ERR_INVALID_URL = "ERR_INVALID_URL";
static readonly ERR_CANCELED = "ERR_CANCELED"; static readonly ERR_CANCELED = "ERR_CANCELED";
static readonly ECONNABORTED = "ECONNABORTED"; static readonly ECONNABORTED = "ECONNABORTED";
static readonly ETIMEDOUT = "ETIMEDOUT"; static readonly ETIMEDOUT = "ETIMEDOUT";
+76 -28
View File
@@ -15,10 +15,15 @@ var VERSION = require('./../env/data').version;
var transitionalDefaults = require('../defaults/transitional'); var transitionalDefaults = require('../defaults/transitional');
var AxiosError = require('../core/AxiosError'); var AxiosError = require('../core/AxiosError');
var CanceledError = require('../cancel/CanceledError'); var CanceledError = require('../cancel/CanceledError');
var platform = require('../platform');
var fromDataURI = require('../helpers/fromDataURI');
var stream = require('stream');
var isHttps = /https:?/; var isHttps = /https:?/;
var supportedProtocols = [ 'http:', 'https:', 'file:' ]; var supportedProtocols = platform.protocols.map(function(protocol) {
return protocol + ':';
});
function dispatchBeforeRedirect(options) { function dispatchBeforeRedirect(options) {
if (options.beforeRedirects.proxy) { if (options.beforeRedirects.proxy) {
@@ -99,6 +104,62 @@ module.exports = function httpAdapter(config) {
rejectPromise(value); rejectPromise(value);
}; };
var data = config.data; var data = config.data;
var responseType = config.responseType;
var responseEncoding = config.responseEncoding;
var method = config.method.toUpperCase();
// Parse url
var fullPath = buildFullPath(config.baseURL, config.url);
var parsed = url.parse(fullPath);
var protocol = parsed.protocol || supportedProtocols[0];
if (protocol === 'data:') {
var convertedData;
if (method !== 'GET') {
return settle(resolve, reject, {
status: 405,
statusText: 'method not allowed',
headers: {},
config: config
});
}
try {
convertedData = fromDataURI(config.url, responseType === 'blob', {
Blob: config.env && config.env.Blob
});
} catch (err) {
throw AxiosError.from(err, AxiosError.ERR_BAD_REQUEST, config);
}
if (responseType === 'text') {
convertedData = convertedData.toString(responseEncoding);
if (!responseEncoding || responseEncoding === 'utf8') {
data = utils.stripBOM(convertedData);
}
} else if (responseType === 'stream') {
convertedData = stream.Readable.from(convertedData);
}
return settle(resolve, reject, {
data: convertedData,
status: 200,
statusText: 'OK',
headers: {},
config: config
});
}
if (supportedProtocols.indexOf(protocol) === -1) {
return reject(new AxiosError(
'Unsupported protocol ' + protocol,
AxiosError.ERR_BAD_REQUEST,
config
));
}
var headers = config.headers; var headers = config.headers;
var headerNames = {}; var headerNames = {};
@@ -159,19 +220,6 @@ module.exports = function httpAdapter(config) {
auth = username + ':' + password; auth = username + ':' + password;
} }
// Parse url
var fullPath = buildFullPath(config.baseURL, config.url);
var parsed = url.parse(fullPath);
var protocol = parsed.protocol || supportedProtocols[0];
if (supportedProtocols.indexOf(protocol) === -1) {
return reject(new AxiosError(
'Unsupported protocol ' + protocol,
AxiosError.ERR_BAD_REQUEST,
config
));
}
if (!auth && parsed.auth) { if (!auth && parsed.auth) {
var urlAuth = parsed.auth.split(':'); var urlAuth = parsed.auth.split(':');
var urlUsername = urlAuth[0] || ''; var urlUsername = urlAuth[0] || '';
@@ -195,7 +243,7 @@ module.exports = function httpAdapter(config) {
var options = { var options = {
path: buildURL(parsed.path, config.params, config.paramsSerializer).replace(/^\?/, ''), path: buildURL(parsed.path, config.params, config.paramsSerializer).replace(/^\?/, ''),
method: config.method.toUpperCase(), method: method,
headers: headers, headers: headers,
agents: { http: config.httpAgent, https: config.httpsAgent }, agents: { http: config.httpAgent, https: config.httpsAgent },
auth: auth, auth: auth,
@@ -242,7 +290,7 @@ module.exports = function httpAdapter(config) {
if (req.aborted) return; if (req.aborted) return;
// uncompress the response body transparently if required // uncompress the response body transparently if required
var stream = res; var responseStream = res;
// return the last request in case of redirects // return the last request in case of redirects
var lastRequest = res.req || req; var lastRequest = res.req || req;
@@ -261,7 +309,7 @@ module.exports = function httpAdapter(config) {
case 'compress': case 'compress':
case 'deflate': case 'deflate':
// add the unzipper to the body stream processing pipeline // add the unzipper to the body stream processing pipeline
stream = stream.pipe(zlib.createUnzip()); responseStream = responseStream.pipe(zlib.createUnzip());
// remove the content-encoding in order to not confuse downstream operations // remove the content-encoding in order to not confuse downstream operations
delete res.headers['content-encoding']; delete res.headers['content-encoding'];
@@ -277,13 +325,13 @@ module.exports = function httpAdapter(config) {
request: lastRequest request: lastRequest
}; };
if (config.responseType === 'stream') { if (responseType === 'stream') {
response.data = stream; response.data = responseStream;
settle(resolve, reject, response); settle(resolve, reject, response);
} else { } else {
var responseBuffer = []; var responseBuffer = [];
var totalResponseBytes = 0; var totalResponseBytes = 0;
stream.on('data', function handleStreamData(chunk) { responseStream.on('data', function handleStreamData(chunk) {
responseBuffer.push(chunk); responseBuffer.push(chunk);
totalResponseBytes += chunk.length; totalResponseBytes += chunk.length;
@@ -291,17 +339,17 @@ module.exports = function httpAdapter(config) {
if (config.maxContentLength > -1 && totalResponseBytes > config.maxContentLength) { if (config.maxContentLength > -1 && totalResponseBytes > config.maxContentLength) {
// stream.destroy() emit aborted event before calling reject() on Node.js v16 // stream.destroy() emit aborted event before calling reject() on Node.js v16
rejected = true; rejected = true;
stream.destroy(); responseStream.destroy();
reject(new AxiosError('maxContentLength size of ' + config.maxContentLength + ' exceeded', reject(new AxiosError('maxContentLength size of ' + config.maxContentLength + ' exceeded',
AxiosError.ERR_BAD_RESPONSE, config, lastRequest)); AxiosError.ERR_BAD_RESPONSE, config, lastRequest));
} }
}); });
stream.on('aborted', function handlerStreamAborted() { responseStream.on('aborted', function handlerStreamAborted() {
if (rejected) { if (rejected) {
return; return;
} }
stream.destroy(); responseStream.destroy();
reject(new AxiosError( reject(new AxiosError(
'maxContentLength size of ' + config.maxContentLength + ' exceeded', 'maxContentLength size of ' + config.maxContentLength + ' exceeded',
AxiosError.ERR_BAD_RESPONSE, AxiosError.ERR_BAD_RESPONSE,
@@ -310,17 +358,17 @@ module.exports = function httpAdapter(config) {
)); ));
}); });
stream.on('error', function handleStreamError(err) { responseStream.on('error', function handleStreamError(err) {
if (req.aborted) return; if (req.aborted) return;
reject(AxiosError.from(err, null, config, lastRequest)); reject(AxiosError.from(err, null, config, lastRequest));
}); });
stream.on('end', function handleStreamEnd() { responseStream.on('end', function handleStreamEnd() {
try { try {
var responseData = responseBuffer.length === 1 ? responseBuffer[0] : Buffer.concat(responseBuffer); var responseData = responseBuffer.length === 1 ? responseBuffer[0] : Buffer.concat(responseBuffer);
if (config.responseType !== 'arraybuffer') { if (responseType !== 'arraybuffer') {
responseData = responseData.toString(config.responseEncoding); responseData = responseData.toString(responseEncoding);
if (!config.responseEncoding || config.responseEncoding === 'utf8') { if (!responseEncoding || responseEncoding === 'utf8') {
responseData = utils.stripBOM(responseData); responseData = utils.stripBOM(responseData);
} }
} }
+2 -1
View File
@@ -11,6 +11,7 @@ var transitionalDefaults = require('../defaults/transitional');
var AxiosError = require('../core/AxiosError'); var AxiosError = require('../core/AxiosError');
var CanceledError = require('../cancel/CanceledError'); var CanceledError = require('../cancel/CanceledError');
var parseProtocol = require('../helpers/parseProtocol'); var parseProtocol = require('../helpers/parseProtocol');
var platform = require('../platform');
module.exports = function xhrAdapter(config) { module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) { return new Promise(function dispatchXhrRequest(resolve, reject) {
@@ -210,7 +211,7 @@ module.exports = function xhrAdapter(config) {
var protocol = parseProtocol(fullPath); var protocol = parseProtocol(fullPath);
if (protocol && [ 'http', 'https', 'file', 'blob' ].indexOf(protocol) === -1) { if (protocol && platform.protocols.indexOf(protocol) === -1) {
reject(new AxiosError('Unsupported protocol ' + protocol + ':', AxiosError.ERR_BAD_REQUEST, config)); reject(new AxiosError('Unsupported protocol ' + protocol + ':', AxiosError.ERR_BAD_REQUEST, config));
return; return;
} }
+5 -1
View File
@@ -64,7 +64,9 @@ var descriptors = {};
'ERR_DEPRECATED', 'ERR_DEPRECATED',
'ERR_BAD_RESPONSE', 'ERR_BAD_RESPONSE',
'ERR_BAD_REQUEST', 'ERR_BAD_REQUEST',
'ERR_CANCELED' 'ERR_CANCELED',
'ERR_NOT_SUPPORT',
'ERR_INVALID_URL'
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
].forEach(function(code) { ].forEach(function(code) {
descriptors[code] = {value: code}; descriptors[code] = {value: code};
@@ -83,6 +85,8 @@ AxiosError.from = function(error, code, config, request, response, customProps)
AxiosError.call(axiosError, error.message, code, config, request, response); AxiosError.call(axiosError, error.message, code, config, request, response);
axiosError.cause = error;
axiosError.name = error.name; axiosError.name = error.name;
customProps && Object.assign(axiosError, customProps); customProps && Object.assign(axiosError, customProps);
+8 -4
View File
@@ -6,6 +6,7 @@ var AxiosError = require('../core/AxiosError');
var transitionalDefaults = require('./transitional'); var transitionalDefaults = require('./transitional');
var toFormData = require('../helpers/toFormData'); var toFormData = require('../helpers/toFormData');
var toURLEncodedForm = require('../helpers/toURLEncodedForm'); var toURLEncodedForm = require('../helpers/toURLEncodedForm');
var platform = require('../platform');
var DEFAULT_CONTENT_TYPE = { var DEFAULT_CONTENT_TYPE = {
'Content-Type': 'application/x-www-form-urlencoded' 'Content-Type': 'application/x-www-form-urlencoded'
@@ -101,11 +102,13 @@ var defaults = {
transformResponse: [function transformResponse(data) { transformResponse: [function transformResponse(data) {
var transitional = this.transitional || defaults.transitional; var transitional = this.transitional || defaults.transitional;
var silentJSONParsing = transitional && transitional.silentJSONParsing;
var forcedJSONParsing = transitional && transitional.forcedJSONParsing; var forcedJSONParsing = transitional && transitional.forcedJSONParsing;
var strictJSONParsing = !silentJSONParsing && this.responseType === 'json'; var JSONRequested = this.responseType === 'json';
if (data && utils.isString(data) && ((forcedJSONParsing && !this.responseType) || JSONRequested)) {
var silentJSONParsing = transitional && transitional.silentJSONParsing;
var strictJSONParsing = !silentJSONParsing && JSONRequested;
if (strictJSONParsing || (forcedJSONParsing && utils.isString(data) && data.length)) {
try { try {
return JSON.parse(data); return JSON.parse(data);
} catch (e) { } catch (e) {
@@ -134,7 +137,8 @@ var defaults = {
maxBodyLength: -1, maxBodyLength: -1,
env: { env: {
FormData: require('./env/FormData') FormData: platform.classes.FormData,
Blob: platform.classes.Blob
}, },
validateStatus: function validateStatus(status) { validateStatus: function validateStatus(status) {
+51
View File
@@ -0,0 +1,51 @@
'use strict';
var AxiosError = require('../core/AxiosError');
var parseProtocol = require('./parseProtocol');
var platform = require('../platform');
var DATA_URL_PATTERN = /^(?:([^;]+);)?(?:[^;]+;)?(base64|),([\s\S]*)$/;
/**
* Parse data uri to a Buffer or Blob
* @param {String} uri
* @param {?Boolean} asBlob
* @param {?Object} options
* @param {?Function} options.Blob
* @returns {Buffer|Blob}
*/
module.exports = function fromDataURI(uri, asBlob, options) {
var _Blob = options && options.Blob || platform.classes.Blob;
var protocol = parseProtocol(uri);
if (asBlob === undefined && _Blob) {
asBlob = true;
}
if (protocol === 'data') {
uri = uri.slice(protocol.length);
var match = DATA_URL_PATTERN.exec(uri);
if (!match) {
throw new AxiosError('Invalid URL', AxiosError.ERR_INVALID_URL);
}
var mime = match[1];
var isBase64 = match[2];
var body = match[3];
var buffer = Buffer.from(decodeURIComponent(body), isBase64 ? 'base64' : 'utf8');
if (asBlob) {
if (!_Blob) {
throw new AxiosError('Blob is not supported', AxiosError.ERR_NOT_SUPPORT);
}
return new _Blob([buffer], {type: mime});
}
return buffer;
}
throw new AxiosError('Unsupported protocol ' + protocol, AxiosError.ERR_NOT_SUPPORT);
};
+1 -1
View File
@@ -1,7 +1,7 @@
'use strict'; 'use strict';
var utils = require('../utils'); var utils = require('../utils');
var envFormData = require('../defaults/env/FormData'); var envFormData = require('../env/classes/FormData');
function isVisitable(thing) { function isVisitable(thing) {
return utils.isPlainObject(thing) || utils.isArray(thing); return utils.isPlainObject(thing) || utils.isArray(thing);
+3
View File
@@ -0,0 +1,3 @@
'use strict';
module.exports = FormData;
+5 -2
View File
@@ -3,6 +3,9 @@
module.exports = { module.exports = {
isBrowser: true, isBrowser: true,
classes: { classes: {
URLSearchParams: require('./classes/URLSearchParams') URLSearchParams: require('./classes/URLSearchParams'),
} FormData: require('./classes/FormData'),
Blob: Blob
},
protocols: ['http', 'https', 'file', 'blob', 'url']
}; };
+3
View File
@@ -0,0 +1,3 @@
'use strict';
module.exports = require('form-data');
+5 -2
View File
@@ -3,6 +3,9 @@
module.exports = { module.exports = {
isNode: true, isNode: true,
classes: { classes: {
URLSearchParams: require('./classes/URLSearchParams') URLSearchParams: require('./classes/URLSearchParams'),
} FormData: require('./classes/FormData'),
Blob: typeof Blob !== 'undefined' && Blob || null
},
protocols: [ 'http', 'https', 'file', 'data' ]
}; };
+8 -7
View File
@@ -229,15 +229,16 @@ function trim(str) {
* navigator.product -> 'NativeScript' or 'NS' * navigator.product -> 'NativeScript' or 'NS'
*/ */
function isStandardBrowserEnv() { function isStandardBrowserEnv() {
if (typeof navigator !== 'undefined' && (navigator.product === 'ReactNative' || var product;
navigator.product === 'NativeScript' || if (typeof navigator !== 'undefined' && (
navigator.product === 'NS')) { (product = navigator.product) === 'ReactNative' ||
product === 'NativeScript' ||
product === 'NS')
) {
return false; return false;
} }
return (
typeof window !== 'undefined' && return typeof window !== 'undefined' && typeof document !== 'undefined';
typeof document !== 'undefined'
);
} }
/** /**
-1
View File
@@ -80,7 +80,6 @@
}, },
"browser": { "browser": {
"./lib/adapters/http.js": "./lib/adapters/xhr.js", "./lib/adapters/http.js": "./lib/adapters/xhr.js",
"./lib/defaults/env/FormData.js": "./lib/helpers/null.js",
"./lib/platform/node/index.js": "./lib/platform/browser/index.js" "./lib/platform/node/index.js": "./lib/platform/browser/index.js"
}, },
"jsdelivr": "dist/axios.min.js", "jsdelivr": "dist/axios.min.js",
+43
View File
@@ -1442,4 +1442,47 @@ describe('supports http with nodejs', function () {
}).catch(done); }).catch(done);
}); });
}); });
describe('Data URL', function () {
it('should support requesting data URL as a Buffer', function (done) {
const buffer = Buffer.from('123');
const dataURI = 'data:application/octet-stream;base64,' + buffer.toString('base64');
axios.get(dataURI).then(({data})=> {
assert.deepStrictEqual(data, buffer);
done();
}).catch(done);
});
it('should support requesting data URL as a String (text)', function (done) {
const buffer = Buffer.from('123', 'utf-8');
const dataURI = 'data:application/octet-stream;base64,' + buffer.toString('base64');
axios.get(dataURI, {responseType: "text"}).then(({data})=> {
assert.deepStrictEqual(data, '123');
done();
}).catch(done);
});
it('should support requesting data URL as a Stream', function (done) {
const buffer = Buffer.from('123', 'utf-8');
const dataURI = 'data:application/octet-stream;base64,' + buffer.toString('base64');
axios.get(dataURI, {responseType: "stream"}).then(({data})=> {
var str = '';
data.on('data', function(response){
str += response.toString();
});
data.on('end', function(){
assert.strictEqual(str, '123');
done();
});
}).catch(done);
});
});
}); });
+12
View File
@@ -0,0 +1,12 @@
var assert = require('assert');
var fromDataURI = require('../../../lib/helpers/fromDataURI');
describe('helpers::fromDataURI', function () {
it('should return buffer from data uri', function () {
const buffer= Buffer.from('123');
const dataURI = 'data:application/octet-stream;base64,' + buffer.toString('base64');
assert.deepStrictEqual(fromDataURI(dataURI), buffer);
});
});