NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // ==UserLibrary== // @name xhrLib // @namespace https://openuserjs.org/users/Anakunda // @copyright 2021, Anakunda (https://openuserjs.org/users/Anakunda) // @license GPL-3.0-or-later // @version 1.24.14 // @author Anakunda // @exclude * // ==/UserScript== // ==/UserLibrary== 'use strict'; var domParser = new DOMParser; var xhrLibmaxRetries = typeof GM_getValue == 'function' ? GM_getValue('xhr_max_retries', 60) : 60; var xhrRetryTimeout = typeof GM_getValue == 'function' ? GM_getValue('xhr_retry_timeout', 1000) : 1000; if (typeof GM_getValue == 'function') var xhrTimeout = GM_getValue('xhr_timeout'); if (!(xhrTimeout > 0)) xhrTimeout = undefined; const recoverableHttpErrors = [ 0, 500, 502, 503, 504, 520, /*521, */522, 523, 524, 525, /*526, */527, 530, ]; // let isBrokenTM = false; // if (typeof GM_info == 'object') try { // isBrokenTM = GM_info.scriptHandler == 'Tampermonkey' && /^(?:4\.12|4\.13)\b/.test(GM_info.version); // } catch(e) { console.warn(e) } function localXHR(url, params, data) { return url ? new Promise(function(resolve, reject) { function getParam(key) { if (key && params && typeof params == 'object') key = key.toLowerCase(); else return undefined; return (key = Object.keys(params).find(_key => _key.toLowerCase() == key)) && params[key]; } function request() { xhr.open(method, url, true); if (accept) xhr.setRequestHeader('Accept', accept); if (contentType) xhr.setRequestHeader('Content-Type', contentType); if (headers && typeof headers == 'object') for (let key in headers) xhr.setRequestHeader(key, headers[key]); xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); xhr.send(data); } function errorHandler() { if (this.statusText == 'Too Many Requests' && (this.status != 0 || this.responseURL) && xhrLibmaxRetries > 0 && ++retryCounter < xhrLibmaxRetries) { defaultErrorHandler(this); setTimeout(request, 5000); } else if (recoverableHttpErrors.includes(this.status) && (this.status != 0 || this.responseURL) && xhrLibmaxRetries > 0 && ++retryCounter < xhrLibmaxRetries) { defaultErrorHandler(this); setTimeout(request, xhrRetryTimeout); } else { const reason = defaultErrorHandler(this); if (this.response && ['json', 'application/json'].includes(this.responseType)) try { console.warn('Error response:', this.response); } catch(e) { } reject(reason); } } const responseSuccess = (xhr, strict = false) => xhr.status >= 200 && xhr.status < 400 || !strict && xhr.status == 0 && !xhr.responseURL; const xhr = new XMLHttpRequest, method = data ? 'POST' : (getParam('method') || 'GET').toUpperCase(); let retryCounter = 0, timeout = getParam('timeout'), noResponse = false, accept, contentType; const responseType = getParam('responseType'); if (method != 'HEAD') switch (responseType) { case null: noResponse = true; break; case undefined: xhr.responseType = 'document'; break; default: if (responseType) xhr.responseType = responseType.toLowerCase(); } switch (xhr.responseType) { case 'document': case 'text/html': case 'html': accept = 'text/html'; break; case 'xml': case 'text/xml': accept = 'text/xml'; break; case 'json': case 'application/json': accept = 'application/json'; break; case 'text': case 'text/plain': accept = 'text/plain'; break; default: accept = '*/*'; } if (!(timeout > 0) && xhrTimeout > 0) timeout = xhrTimeout; if (timeout > 0) xhr.timeout = timeout; if (data === undefined) data = getParam('data') || getParam('body'); if (data !== undefined) { if (data instanceof FormData) { //contentType = 'multipart/form-data; charset=utf-8'; } else if (data instanceof URLSearchParams) { //data = data.toString(); //contentType = 'application/x-www-form-urlencoded; charset=utf-8'; } else if (data instanceof ArrayBuffer) { //contentType = 'application/arraybuffer'; } else if (data instanceof Blob || data instanceof File) { //contentType = 'application/arraybuffer'; } else if (typeof data == 'object') { data = JSON.stringify(data); contentType = 'application/json; charset=UTF-8'; } else if (typeof data == 'string') { contentType = 'text/plain; charset=UTF-8'; } } const headers = getParam('headers'); if (method == 'HEAD') { if (xhr.responseType) xhr.responseType = ''; xhr.onreadystatechange = function() { if (this.readyState != XMLHttpRequest.HEADERS_RECEIVED) return; if (responseSuccess(this)) resolve(this); else { errorHandler.call(this); this.abort(); } }; } else if (noResponse) xhr.onreadystatechange = function() { if (this.readyState != XMLHttpRequest.DONE) return; if (responseSuccess(this)) resolve(this.status); else errorHandler.call(this); }; else xhr.onload = function() { if (responseSuccess(this)) resolve(this.response); else errorHandler.call(this); }; xhr.onerror = errorHandler; xhr.ontimeout = function() { /*if (xhrLibmaxRetries > 0 && ++retryCounter < xhrLibmaxRetries) { defaultTimeoutHandler(this); setTimeout(request, xhrRetryTimeout); } else */reject(defaultTimeoutHandler(this)); }; //console.debug('XMLHttpRequest.send(...):', xhr); request(); }) : Promise.reject(new Error('URL missing')); } function globalXHR(url, params, data) { return url ? new Promise(function(resolve, reject) { function addFunc(response) { response.getHeaderValue = function(key) { if (!key || typeof key != 'string') return undefined; let entries = this.responseHeaders.split(/\r?\n/) .map(line => /^(\S+)\s*:\s*(.*)$/.test(line) ? [RegExp.$1.toLowerCase(), RegExp.$2] : null) .filter(Array.isArray); return new Map(entries).get(key.toLowerCase()); }; } function errorHandler(response) { if (response.statusText == 'Too Many Requests' && (response.status != 0 || response.finalUrl) && xhrLibmaxRetries > 0 && ++retryCounter < xhrLibmaxRetries) { defaultErrorHandler(response); console.log('globalXHR retry', retryCounter, '/', xhrLibmaxRetries, '(' + response.status + ')'); setTimeout(() => { hXHR = GM_xmlhttpRequest(localParams) }, xhrRetryTimeout); } else if (recoverableHttpErrors.includes(response.status) && (response.status != 0 || response.finalUrl) && xhrLibmaxRetries > 0 && ++retryCounter < xhrLibmaxRetries) { defaultErrorHandler(response); console.log('globalXHR retry', retryCounter, '/', xhrLibmaxRetries, '(' + response.status + ')'); setTimeout(() => { hXHR = GM_xmlhttpRequest(localParams) }, xhrRetryTimeout); } else { const reason = defaultErrorHandler(response); if (response.responseText && ['json', 'application/json'].includes(localParams.responseType)) try { console.warn('Error response:', response.response); } catch(e) { } reject(reason); } } const responseSuccess = (response, strict = false) => response.status >= 200 && response.status < 400 || !strict && response.status == 0 && !response.finalUrl; const localParams = Object.assign({ url: url }, params); localParams.headers = Object.assign({ }, params && params.headers); localParams.method = data ? 'POST' : localParams.method ? localParams.method.toUpperCase() : 'GET'; let retryCounter = 0, noResponse = false; if (localParams.method != 'HEAD') switch (localParams.responseType) { case null: delete localParams.responseType; noResponse = true; break; case undefined: localParams.responseType = 'document'; break; default: if (localParams.responseType) localParams.responseType = localParams.responseType.toLowerCase(); else delete localParams.responseType; } //if (isBrokenTM && localParams.responseType == 'document') localParams.responseType = 'text/html'; if (typeof localParams.headers != 'object') localParams.headers = { }; if (!localParams.headers.Accept) switch (localParams.responseType) { case 'document': case 'text/html': case 'html': localParams.headers.Accept = 'text/html'; break; case 'xml': case 'text/xml': localParams.headers.Accept = 'text/xml'; break; case 'json': case 'application/json': localParams.headers.Accept = 'application/json'; break; case 'text': case 'text/plain': localParams.headers.Accept = 'text/plain'; break; default: localParams.headers.Accept = '*/*'; } if (!localParams.fetch) localParams.headers['X-Requested-With'] = 'XMLHttpRequest'; if (localParams.method == 'HEAD') { if ('responseType' in localParams) delete localParams.responseType; localParams.onreadystatechange = function(response) { if (response.readyState != XMLHttpRequest.HEADERS_RECEIVED) return; if (responseSuccess(response)) { addFunc(response); resolve(response); } else { hXHR.abort(); errorHandler(response); } }; } else if (noResponse) localParams.onreadystatechange = function(response) { if (response.readyState != XMLHttpRequest.DONE) return; if (responseSuccess(response)) resolve(response.status); else errorHandler(response); }; else localParams.onload = function(response) { if (responseSuccess(response)) { switch (localParams.responseType) { case 'document': case 'text/html': case 'html': if (response.response instanceof HTMLDocument) response.document = response.response; else if (response.responseText) try { response.document = domParser.parseFromString(response.responseText, 'text/html'); } catch(e) { console.warn('globalXHR(...) response not valid HTML:', response.responseText, e); response.document = null; } break; case 'json': case 'application/json': if (typeof response.response == 'object') response.json = response.response; else if (response.responseText) try { response.json = JSON.parse(response.responseText); } catch(e) { console.warn('globalXHR(...) response not valid JSON:', response.responseText, e); response.json = null; } break; case 'xml': case 'text/xml': if (response.response instanceof XMLDocument) response.xml = response.response; else if (response.responseText) try { response.xml = domParser.parseFromString(response.responseText, 'text/xml'); } catch(e) { console.warn('globalXHR(...) response not valid XML:', response.responseText, e); response.xml = null; } break; case 'text': case 'text/plain': break; } addFunc(response); resolve(response); } else errorHandler(response); }; localParams.onerror = errorHandler; localParams.ontimeout = function(response) { /*if (xhrLibmaxRetries > 0 && ++retryCounter < xhrLibmaxRetries) { defaultTimeoutHandler(response); console.log('globalXHR retry', retryCounter, '/', xhrLibmaxRetries, response.status); setTimeout(() => { hXHR = GM_xmlhttpRequest(localParams) }, xhrRetryTimeout); } else */reject(defaultTimeoutHandler(response)); }; if (data === undefined) data = localParams.body; if (data) { if (data instanceof FormData) { //localParams.headers['Content-Type'] = 'multipart/form-data; charset=utf-8'; localParams.data = data; } else if (data instanceof URLSearchParams) { localParams.data = data.toString(); localParams.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8'; } else if (data instanceof ArrayBuffer) { localParams.data = data; } else if (data instanceof Blob || data instanceof File) { localParams.data = data; } else if (typeof data == 'object') { localParams.data = JSON.stringify(data); localParams.headers['Content-Type'] = 'application/json; charset=UTF-8'; } else if (typeof data == 'string') { localParams.data = data; localParams.headers['Content-Type'] = 'text/plain; charset=UTF-8'; } else localParams.data = data; } if ('body' in localParams) delete localParams.body; if (!(localParams.timeout > 0) && xhrTimeout > 0) localParams.timeout = xhrTimeout; //console.debug('GM_xmlhttpRequest(...):', localParams); var hXHR = GM_xmlhttpRequest(localParams); }) : Promise.reject('URL missing'); } function fileReader(filePath, params) { const unc = new URL('file:' + filePath); return globalXHR(unc, params); } function textFileReader(filePath, params) { return fileReader(filePath, Object.assign(params || { }, { responseType: 'text' })) .then(response => response.responseText); } function defaultErrorHandler(response) { console.error('HTTP error:', response); let reason = 'HTTP error ' + response.status; if (response.status == 0) reason += '/' + response.readyState; let statusText = response.statusText; if (response.response) try { if (typeof response.response.error == 'string') statusText = response.response.error; } catch(e) { } if (statusText) reason += ' (' + statusText + ')'; return reason; } function defaultTimeoutHandler(response) { console.error('HTTP timeout:', response); let reason = 'HTTP timeout'; if (response.timeout) reason += ' (' + response.timeout + ')'; return reason; } ////////////////////////////////////////////////////////////////////////// ////////////////////////////// SAFE PADDING ////////////////////////////// //////////////////////////////////////////////////////////////////////////