From: Jo-Philipp Wich Date: Mon, 21 Feb 2022 13:59:16 +0000 (+0100) Subject: luci-base: properly handle promise targets in Request.request() X-Git-Url: http://git.lede-project.org./?a=commitdiff_plain;h=1b6c126293edc9abc3b23b195a1dbe2cedf1756e;p=project%2Fluci.git 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 (backported from commit 5663fd596b567d53587fcc4052df3095520c08a7) --- diff --git a/modules/luci-base/htdocs/luci-static/resources/luci.js b/modules/luci-base/htdocs/luci-static/resources/luci.js index 8f8b9673d6..7c334fab3c 100644 --- a/modules/luci-base/htdocs/luci-static/resources/luci.js +++ b/modules/luci-base/htdocs/luci-static/resources/luci.js @@ -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 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]); - - 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; - - default: - if (content == null) { - content = q; - contenttype = 'application/x-www-form-urlencoded'; + 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(); + + 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('&') : ''; + + 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'; + } } } } - } - 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 (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); + 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'; + opt.xhr.responseType = opt.responseType || 'text'; - if ('overrideMimeType' in opt.xhr) - opt.xhr.overrideMimeType('application/octet-stream'); + if ('overrideMimeType' in opt.xhr) + opt.xhr.overrideMimeType('application/octet-stream'); - if ('timeout' in opt) - opt.xhr.timeout = +opt.timeout; + if ('timeout' in opt) + opt.xhr.timeout = +opt.timeout; - if ('credentials' in opt) - opt.xhr.withCredentials = !!opt.credentials; + if ('credentials' in opt) + opt.xhr.withCredentials = !!opt.credentials; - if (opt.content != null) { - switch (typeof(opt.content)) { - case 'function': - content = opt.content(xhr); - break; + if (opt.content != null) { + switch (typeof(opt.content)) { + case 'function': + content = opt.content(xhr); + break; - case 'object': - if (!(opt.content instanceof FormData)) { - content = JSON.stringify(opt.content); - contenttype = 'application/json'; - } - else { - content = opt.content; - } - 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); + default: + content = String(opt.content); + } } - } - 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 ('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 ('progress' in opt && 'upload' in opt.xhr) - opt.xhr.upload.addEventListener('progress', opt.progress); + if ('progress' in opt && 'upload' in opt.xhr) + opt.xhr.upload.addEventListener('progress', opt.progress); - if (contenttype != null) - opt.xhr.setRequestHeader('Content-Type', contenttype); + if (contenttype != null) + opt.xhr.setRequestHeader('Content-Type', contenttype); - try { - opt.xhr.send(content); - } - catch (e) { - rejectFn.call(opt, e); - } - }); + try { + opt.xhr.send(content); + } + catch (e) { + rejectFn.call(opt, e); + } + }); + }).bind(this)); }, handleReadyStateChange: function(resolveFn, rejectFn, ev) {