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>
This commit is contained in:
parent
e870775d39
commit
5663fd596b
1 changed files with 95 additions and 93 deletions
|
@ -695,115 +695,117 @@
|
||||||
* The resulting HTTP response.
|
* The resulting HTTP response.
|
||||||
*/
|
*/
|
||||||
request: function(target, options) {
|
request: function(target, options) {
|
||||||
var state = { xhr: new XMLHttpRequest(), url: this.expandURL(target), start: Date.now() },
|
return Promise.resolve(target).then((function(url) {
|
||||||
opt = Object.assign({}, options, state),
|
var state = { xhr: new XMLHttpRequest(), url: this.expandURL(url), start: Date.now() },
|
||||||
content = null,
|
opt = Object.assign({}, options, state),
|
||||||
contenttype = null,
|
content = null,
|
||||||
callback = this.handleReadyStateChange;
|
contenttype = null,
|
||||||
|
callback = this.handleReadyStateChange;
|
||||||
|
|
||||||
return new Promise(function(resolveFn, rejectFn) {
|
return new Promise(function(resolveFn, rejectFn) {
|
||||||
opt.xhr.onreadystatechange = callback.bind(opt, resolveFn, rejectFn);
|
opt.xhr.onreadystatechange = callback.bind(opt, resolveFn, rejectFn);
|
||||||
opt.method = String(opt.method || 'GET').toUpperCase();
|
opt.method = String(opt.method || 'GET').toUpperCase();
|
||||||
|
|
||||||
if ('query' in opt) {
|
if ('query' in opt) {
|
||||||
var q = (opt.query != null) ? Object.keys(opt.query).map(function(k) {
|
var q = (opt.query != null) ? Object.keys(opt.query).map(function(k) {
|
||||||
if (opt.query[k] != null) {
|
if (opt.query[k] != null) {
|
||||||
var v = (typeof(opt.query[k]) == 'object')
|
var v = (typeof(opt.query[k]) == 'object')
|
||||||
? JSON.stringify(opt.query[k])
|
? JSON.stringify(opt.query[k])
|
||||||
: String(opt.query[k]);
|
: String(opt.query[k]);
|
||||||
|
|
||||||
return '%s=%s'.format(encodeURIComponent(k), encodeURIComponent(v));
|
return '%s=%s'.format(encodeURIComponent(k), encodeURIComponent(v));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return encodeURIComponent(k);
|
return encodeURIComponent(k);
|
||||||
}
|
}
|
||||||
}).join('&') : '';
|
}).join('&') : '';
|
||||||
|
|
||||||
if (q !== '') {
|
if (q !== '') {
|
||||||
switch (opt.method) {
|
switch (opt.method) {
|
||||||
case 'GET':
|
case 'GET':
|
||||||
case 'HEAD':
|
case 'HEAD':
|
||||||
case 'OPTIONS':
|
case 'OPTIONS':
|
||||||
opt.url += ((/\?/).test(opt.url) ? '&' : '?') + q;
|
opt.url += ((/\?/).test(opt.url) ? '&' : '?') + q;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
if (content == null) {
|
if (content == null) {
|
||||||
content = q;
|
content = q;
|
||||||
contenttype = 'application/x-www-form-urlencoded';
|
contenttype = 'application/x-www-form-urlencoded';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!opt.cache)
|
if (!opt.cache)
|
||||||
opt.url += ((/\?/).test(opt.url) ? '&' : '?') + (new Date()).getTime();
|
opt.url += ((/\?/).test(opt.url) ? '&' : '?') + (new Date()).getTime();
|
||||||
|
|
||||||
if (isQueueableRequest(opt)) {
|
if (isQueueableRequest(opt)) {
|
||||||
requestQueue.push([opt, rejectFn, resolveFn]);
|
requestQueue.push([opt, rejectFn, resolveFn]);
|
||||||
requestAnimationFrame(flushRequestQueue);
|
requestAnimationFrame(flushRequestQueue);
|
||||||
return;
|
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 ('headers' in opt)
|
if ('username' in opt && 'password' in opt)
|
||||||
for (var header in opt.headers)
|
opt.xhr.open(opt.method, opt.url, true, opt.username, opt.password);
|
||||||
if (opt.headers.hasOwnProperty(header)) {
|
else
|
||||||
if (header.toLowerCase() != 'content-type')
|
opt.xhr.open(opt.method, opt.url, true);
|
||||||
opt.xhr.setRequestHeader(header, opt.headers[header]);
|
|
||||||
else
|
opt.xhr.responseType = opt.responseType || 'text';
|
||||||
contenttype = opt.headers[header];
|
|
||||||
|
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)
|
if ('headers' in opt)
|
||||||
opt.xhr.upload.addEventListener('progress', opt.progress);
|
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)
|
if ('progress' in opt && 'upload' in opt.xhr)
|
||||||
opt.xhr.setRequestHeader('Content-Type', contenttype);
|
opt.xhr.upload.addEventListener('progress', opt.progress);
|
||||||
|
|
||||||
try {
|
if (contenttype != null)
|
||||||
opt.xhr.send(content);
|
opt.xhr.setRequestHeader('Content-Type', contenttype);
|
||||||
}
|
|
||||||
catch (e) {
|
try {
|
||||||
rejectFn.call(opt, e);
|
opt.xhr.send(content);
|
||||||
}
|
}
|
||||||
});
|
catch (e) {
|
||||||
|
rejectFn.call(opt, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).bind(this));
|
||||||
},
|
},
|
||||||
|
|
||||||
handleReadyStateChange: function(resolveFn, rejectFn, ev) {
|
handleReadyStateChange: function(resolveFn, rejectFn, ev) {
|
||||||
|
|
Loading…
Reference in a new issue