Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added data URL support for node.js; #4725

Merged
merged 4 commits into from May 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc.js
Expand Up @@ -80,7 +80,7 @@ module.exports = {
'no-new-wrappers': 2, // http://eslint.org/docs/rules/no-new-wrappers
'no-octal': 2, // http://eslint.org/docs/rules/no-octal
'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-redeclare': 2, // http://eslint.org/docs/rules/no-redeclare
'no-return-assign': 2, // http://eslint.org/docs/rules/no-return-assign
Expand Down
3 changes: 3 additions & 0 deletions index.d.ts
Expand Up @@ -194,13 +194,16 @@ export class AxiosError<T = unknown, D = any> extends Error {
isAxiosError: boolean;
status?: number;
toJSON: () => object;
cause?: Error;
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 = "ERR_BAD_OPTION";
static readonly ERR_NETWORK = "ERR_NETWORK";
static readonly ERR_DEPRECATED = "ERR_DEPRECATED";
static readonly ERR_BAD_RESPONSE = "ERR_BAD_RESPONSE";
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 ECONNABORTED = "ECONNABORTED";
static readonly ETIMEDOUT = "ETIMEDOUT";
Expand Down
104 changes: 76 additions & 28 deletions lib/adapters/http.js
Expand Up @@ -15,10 +15,15 @@ var VERSION = require('./../env/data').version;
var transitionalDefaults = require('../defaults/transitional');
var AxiosError = require('../core/AxiosError');
var CanceledError = require('../cancel/CanceledError');
var platform = require('../platform');
var fromDataURI = require('../helpers/fromDataURI');
var stream = require('stream');

var isHttps = /https:?/;

var supportedProtocols = [ 'http:', 'https:', 'file:' ];
var supportedProtocols = platform.protocols.map(function(protocol) {
return protocol + ':';
});

function dispatchBeforeRedirect(options) {
if (options.beforeRedirects.proxy) {
Expand Down Expand Up @@ -99,6 +104,62 @@ module.exports = function httpAdapter(config) {
rejectPromise(value);
};
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 headerNames = {};

Expand Down Expand Up @@ -159,19 +220,6 @@ module.exports = function httpAdapter(config) {
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) {
var urlAuth = parsed.auth.split(':');
var urlUsername = urlAuth[0] || '';
Expand All @@ -195,7 +243,7 @@ module.exports = function httpAdapter(config) {

var options = {
path: buildURL(parsed.path, config.params, config.paramsSerializer).replace(/^\?/, ''),
method: config.method.toUpperCase(),
method: method,
headers: headers,
agents: { http: config.httpAgent, https: config.httpsAgent },
auth: auth,
Expand Down Expand Up @@ -242,7 +290,7 @@ module.exports = function httpAdapter(config) {
if (req.aborted) return;

// uncompress the response body transparently if required
var stream = res;
var responseStream = res;

// return the last request in case of redirects
var lastRequest = res.req || req;
Expand All @@ -261,7 +309,7 @@ module.exports = function httpAdapter(config) {
case 'compress':
case 'deflate':
// 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
delete res.headers['content-encoding'];
Expand All @@ -277,31 +325,31 @@ module.exports = function httpAdapter(config) {
request: lastRequest
};

if (config.responseType === 'stream') {
response.data = stream;
if (responseType === 'stream') {
response.data = responseStream;
settle(resolve, reject, response);
} else {
var responseBuffer = [];
var totalResponseBytes = 0;
stream.on('data', function handleStreamData(chunk) {
responseStream.on('data', function handleStreamData(chunk) {
responseBuffer.push(chunk);
totalResponseBytes += chunk.length;

// make sure the content length is not over the maxContentLength if specified
if (config.maxContentLength > -1 && totalResponseBytes > config.maxContentLength) {
// stream.destroy() emit aborted event before calling reject() on Node.js v16
rejected = true;
stream.destroy();
responseStream.destroy();
reject(new AxiosError('maxContentLength size of ' + config.maxContentLength + ' exceeded',
AxiosError.ERR_BAD_RESPONSE, config, lastRequest));
}
});

stream.on('aborted', function handlerStreamAborted() {
responseStream.on('aborted', function handlerStreamAborted() {
if (rejected) {
return;
}
stream.destroy();
responseStream.destroy();
reject(new AxiosError(
'maxContentLength size of ' + config.maxContentLength + ' exceeded',
AxiosError.ERR_BAD_RESPONSE,
Expand All @@ -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;
reject(AxiosError.from(err, null, config, lastRequest));
});

stream.on('end', function handleStreamEnd() {
responseStream.on('end', function handleStreamEnd() {
try {
var responseData = responseBuffer.length === 1 ? responseBuffer[0] : Buffer.concat(responseBuffer);
if (config.responseType !== 'arraybuffer') {
responseData = responseData.toString(config.responseEncoding);
if (!config.responseEncoding || config.responseEncoding === 'utf8') {
if (responseType !== 'arraybuffer') {
responseData = responseData.toString(responseEncoding);
if (!responseEncoding || responseEncoding === 'utf8') {
responseData = utils.stripBOM(responseData);
}
}
Expand Down
3 changes: 2 additions & 1 deletion lib/adapters/xhr.js
Expand Up @@ -11,6 +11,7 @@ var transitionalDefaults = require('../defaults/transitional');
var AxiosError = require('../core/AxiosError');
var CanceledError = require('../cancel/CanceledError');
var parseProtocol = require('../helpers/parseProtocol');
var platform = require('../platform');

module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
Expand Down Expand Up @@ -210,7 +211,7 @@ module.exports = function xhrAdapter(config) {

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));
return;
}
Expand Down
6 changes: 5 additions & 1 deletion lib/core/AxiosError.js
Expand Up @@ -64,7 +64,9 @@ var descriptors = {};
'ERR_DEPRECATED',
'ERR_BAD_RESPONSE',
'ERR_BAD_REQUEST',
'ERR_CANCELED'
'ERR_CANCELED',
'ERR_NOT_SUPPORT',
'ERR_INVALID_URL'
// eslint-disable-next-line func-names
].forEach(function(code) {
descriptors[code] = {value: code};
Expand All @@ -83,6 +85,8 @@ AxiosError.from = function(error, code, config, request, response, customProps)

AxiosError.call(axiosError, error.message, code, config, request, response);

axiosError.cause = error;

axiosError.name = error.name;

customProps && Object.assign(axiosError, customProps);
Expand Down
12 changes: 8 additions & 4 deletions lib/defaults/index.js
Expand Up @@ -6,6 +6,7 @@ var AxiosError = require('../core/AxiosError');
var transitionalDefaults = require('./transitional');
var toFormData = require('../helpers/toFormData');
var toURLEncodedForm = require('../helpers/toURLEncodedForm');
var platform = require('../platform');

var DEFAULT_CONTENT_TYPE = {
'Content-Type': 'application/x-www-form-urlencoded'
Expand Down Expand Up @@ -101,11 +102,13 @@ var defaults = {

transformResponse: [function transformResponse(data) {
var transitional = this.transitional || defaults.transitional;
var silentJSONParsing = transitional && transitional.silentJSONParsing;
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 {
return JSON.parse(data);
} catch (e) {
Expand Down Expand Up @@ -134,7 +137,8 @@ var defaults = {
maxBodyLength: -1,

env: {
FormData: require('./env/FormData')
FormData: platform.classes.FormData,
Blob: platform.classes.Blob
},

validateStatus: function validateStatus(status) {
Expand Down
File renamed without changes.
51 changes: 51 additions & 0 deletions lib/helpers/fromDataURI.js
@@ -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);
};
2 changes: 1 addition & 1 deletion lib/helpers/toFormData.js
@@ -1,7 +1,7 @@
'use strict';

var utils = require('../utils');
var envFormData = require('../defaults/env/FormData');
var envFormData = require('../env/classes/FormData');

function isVisitable(thing) {
return utils.isPlainObject(thing) || utils.isArray(thing);
Expand Down
3 changes: 3 additions & 0 deletions lib/platform/browser/classes/FormData.js
@@ -0,0 +1,3 @@
'use strict';

module.exports = FormData;
7 changes: 5 additions & 2 deletions lib/platform/browser/index.js
Expand Up @@ -3,6 +3,9 @@
module.exports = {
isBrowser: true,
classes: {
URLSearchParams: require('./classes/URLSearchParams')
}
URLSearchParams: require('./classes/URLSearchParams'),
FormData: require('./classes/FormData'),
Blob: Blob
},
protocols: ['http', 'https', 'file', 'blob', 'url']
};
3 changes: 3 additions & 0 deletions lib/platform/node/classes/FormData.js
@@ -0,0 +1,3 @@
'use strict';

module.exports = require('form-data');
7 changes: 5 additions & 2 deletions lib/platform/node/index.js
Expand Up @@ -3,6 +3,9 @@
module.exports = {
isNode: true,
classes: {
URLSearchParams: require('./classes/URLSearchParams')
}
URLSearchParams: require('./classes/URLSearchParams'),
FormData: require('./classes/FormData'),
Blob: typeof Blob !== 'undefined' && Blob || null
},
protocols: [ 'http', 'https', 'file', 'data' ]
};