luci-base: properly handle promise targets in Request.request()

Under some circumstances, ubus RPC requests may be initiated while LuCI is
still resolving the `rpcBaseURL` value. In this situation, the `target`
argument of the `request()` call will be a pending promise object which
results in an invalid URL when serialized by `expandURL()`, leading to an
`Failed to execute 'open' on 'XMLHttpRequest': Invalid URL` exception.

This commonly occured on the index status page which immediately initiates
ubus RPC calls on load to discover existing status page partials.

Solve the issue by filtering the given `target` argument through
`Promise.resolve()` before expanding the URL and initiating the actual
request.

Fixes: #3747
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
(cherry picked from commit 5663fd596b)
This commit is contained in:
Jo-Philipp Wich 2022-02-21 14:59:16 +01:00
parent cc582ebfb3
commit 31a27f3087

View file

@ -695,115 +695,117 @@
* The resulting HTTP response.
*/
request: function(target, options) {
var state = { xhr: new XMLHttpRequest(), url: this.expandURL(target), start: Date.now() },
opt = Object.assign({}, options, state),
content = null,
contenttype = null,
callback = this.handleReadyStateChange;
return Promise.resolve(target).then((function(url) {
var state = { xhr: new XMLHttpRequest(), url: this.expandURL(url), start: Date.now() },
opt = Object.assign({}, options, state),
content = null,
contenttype = null,
callback = this.handleReadyStateChange;
return new Promise(function(resolveFn, rejectFn) {
opt.xhr.onreadystatechange = callback.bind(opt, resolveFn, rejectFn);
opt.method = String(opt.method || 'GET').toUpperCase();
return new Promise(function(resolveFn, rejectFn) {
opt.xhr.onreadystatechange = callback.bind(opt, resolveFn, rejectFn);
opt.method = String(opt.method || 'GET').toUpperCase();
if ('query' in opt) {
var q = (opt.query != null) ? Object.keys(opt.query).map(function(k) {
if (opt.query[k] != null) {
var v = (typeof(opt.query[k]) == 'object')
? JSON.stringify(opt.query[k])
: String(opt.query[k]);
if ('query' in opt) {
var q = (opt.query != null) ? Object.keys(opt.query).map(function(k) {
if (opt.query[k] != null) {
var v = (typeof(opt.query[k]) == 'object')
? JSON.stringify(opt.query[k])
: String(opt.query[k]);
return '%s=%s'.format(encodeURIComponent(k), encodeURIComponent(v));
}
else {
return encodeURIComponent(k);
}
}).join('&') : '';
return '%s=%s'.format(encodeURIComponent(k), encodeURIComponent(v));
}
else {
return encodeURIComponent(k);
}
}).join('&') : '';
if (q !== '') {
switch (opt.method) {
case 'GET':
case 'HEAD':
case 'OPTIONS':
opt.url += ((/\?/).test(opt.url) ? '&' : '?') + q;
break;
if (q !== '') {
switch (opt.method) {
case 'GET':
case 'HEAD':
case 'OPTIONS':
opt.url += ((/\?/).test(opt.url) ? '&' : '?') + q;
break;
default:
if (content == null) {
content = q;
contenttype = 'application/x-www-form-urlencoded';
default:
if (content == null) {
content = q;
contenttype = 'application/x-www-form-urlencoded';
}
}
}
}
}
if (!opt.cache)
opt.url += ((/\?/).test(opt.url) ? '&' : '?') + (new Date()).getTime();
if (!opt.cache)
opt.url += ((/\?/).test(opt.url) ? '&' : '?') + (new Date()).getTime();
if (isQueueableRequest(opt)) {
requestQueue.push([opt, rejectFn, resolveFn]);
requestAnimationFrame(flushRequestQueue);
return;
}
if ('username' in opt && 'password' in opt)
opt.xhr.open(opt.method, opt.url, true, opt.username, opt.password);
else
opt.xhr.open(opt.method, opt.url, true);
opt.xhr.responseType = opt.responseType || 'text';
if ('overrideMimeType' in opt.xhr)
opt.xhr.overrideMimeType('application/octet-stream');
if ('timeout' in opt)
opt.xhr.timeout = +opt.timeout;
if ('credentials' in opt)
opt.xhr.withCredentials = !!opt.credentials;
if (opt.content != null) {
switch (typeof(opt.content)) {
case 'function':
content = opt.content(opt.xhr);
break;
case 'object':
if (!(opt.content instanceof FormData)) {
content = JSON.stringify(opt.content);
contenttype = 'application/json';
}
else {
content = opt.content;
}
break;
default:
content = String(opt.content);
if (isQueueableRequest(opt)) {
requestQueue.push([opt, rejectFn, resolveFn]);
requestAnimationFrame(flushRequestQueue);
return;
}
}
if ('headers' in opt)
for (var header in opt.headers)
if (opt.headers.hasOwnProperty(header)) {
if (header.toLowerCase() != 'content-type')
opt.xhr.setRequestHeader(header, opt.headers[header]);
else
contenttype = opt.headers[header];
if ('username' in opt && 'password' in opt)
opt.xhr.open(opt.method, opt.url, true, opt.username, opt.password);
else
opt.xhr.open(opt.method, opt.url, true);
opt.xhr.responseType = opt.responseType || 'text';
if ('overrideMimeType' in opt.xhr)
opt.xhr.overrideMimeType('application/octet-stream');
if ('timeout' in opt)
opt.xhr.timeout = +opt.timeout;
if ('credentials' in opt)
opt.xhr.withCredentials = !!opt.credentials;
if (opt.content != null) {
switch (typeof(opt.content)) {
case 'function':
content = opt.content(opt.xhr);
break;
case 'object':
if (!(opt.content instanceof FormData)) {
content = JSON.stringify(opt.content);
contenttype = 'application/json';
}
else {
content = opt.content;
}
break;
default:
content = String(opt.content);
}
}
if ('progress' in opt && 'upload' in opt.xhr)
opt.xhr.upload.addEventListener('progress', opt.progress);
if ('headers' in opt)
for (var header in opt.headers)
if (opt.headers.hasOwnProperty(header)) {
if (header.toLowerCase() != 'content-type')
opt.xhr.setRequestHeader(header, opt.headers[header]);
else
contenttype = opt.headers[header];
}
if (contenttype != null)
opt.xhr.setRequestHeader('Content-Type', contenttype);
if ('progress' in opt && 'upload' in opt.xhr)
opt.xhr.upload.addEventListener('progress', opt.progress);
try {
opt.xhr.send(content);
}
catch (e) {
rejectFn.call(opt, e);
}
});
if (contenttype != null)
opt.xhr.setRequestHeader('Content-Type', contenttype);
try {
opt.xhr.send(content);
}
catch (e) {
rejectFn.call(opt, e);
}
});
}).bind(this));
},
handleReadyStateChange: function(resolveFn, rejectFn, ev) {