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 //////////////////////////////
//////////////////////////////////////////////////////////////////////////