Compare commits
No commits in common. "3ef3e074e6be89acb9894267936719517ffa1472" and "8f500c590e28ddb9d6203ea67ba51441ac06557e" have entirely different histories.
3ef3e074e6
...
8f500c590e
@ -25,7 +25,6 @@ export default [
|
|||||||
URL: 'readonly',
|
URL: 'readonly',
|
||||||
fetch: 'readonly',
|
fetch: 'readonly',
|
||||||
AbortController: 'readonly',
|
AbortController: 'readonly',
|
||||||
AbortSignal: 'readonly',
|
|
||||||
navigator: 'readonly',
|
navigator: 'readonly',
|
||||||
document: 'readonly',
|
document: 'readonly',
|
||||||
window: 'readonly',
|
window: 'readonly',
|
||||||
|
|||||||
@ -76,27 +76,11 @@ class DoodstreamUploader {
|
|||||||
headers['Cookie'] = this._cookieHeader();
|
headers['Cookie'] = this._cookieHeader();
|
||||||
}
|
}
|
||||||
|
|
||||||
// The small discovery/result requests that bookend a multi-minute upload
|
const res = await fetch(url, {
|
||||||
// occasionally hit a transient blip ("fetch failed", ECONNRESET, a hung TLS
|
...opts,
|
||||||
// handshake). A blip here shouldn't throw away the whole upload, so retry a
|
headers,
|
||||||
// few times with short backoff. Each attempt gets its own 20s timeout —
|
redirect: 'manual'
|
||||||
// Node's fetch has none by default, and a hung socket would otherwise stall
|
});
|
||||||
// the attempt for minutes. The big file upload (undici) is retried at the
|
|
||||||
// upload-manager level, not here.
|
|
||||||
let res;
|
|
||||||
for (let attempt = 1; ; attempt++) {
|
|
||||||
const timeoutSignal = AbortSignal.timeout(20000);
|
|
||||||
const signal = opts.signal ? AbortSignal.any([opts.signal, timeoutSignal]) : timeoutSignal;
|
|
||||||
try {
|
|
||||||
res = await fetch(url, { ...opts, headers, redirect: 'manual', signal });
|
|
||||||
break;
|
|
||||||
} catch (err) {
|
|
||||||
if (opts.signal && opts.signal.aborted) throw err; // caller abort: don't retry
|
|
||||||
if (attempt >= 3) throw err;
|
|
||||||
_debugLog(`_fetch transient (${attempt}/3) ${url}: ${err && err.message}; retry`);
|
|
||||||
await new Promise(r => setTimeout(r, 400 * attempt));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this._parseCookiesFromHeaders(res.headers);
|
this._parseCookiesFromHeaders(res.headers);
|
||||||
|
|
||||||
@ -312,31 +296,19 @@ class DoodstreamUploader {
|
|||||||
yield epilogueBuf;
|
yield epilogueBuf;
|
||||||
}
|
}
|
||||||
|
|
||||||
let uploadRes;
|
const uploadRes = await request(uploadUrl, {
|
||||||
try {
|
method: 'POST',
|
||||||
uploadRes = await request(uploadUrl, {
|
headers: {
|
||||||
method: 'POST',
|
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
||||||
headers: {
|
'Content-Length': String(totalSize),
|
||||||
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
'User-Agent': USER_AGENT,
|
||||||
'Content-Length': String(totalSize),
|
'Cookie': this._cookieHeader()
|
||||||
'User-Agent': USER_AGENT,
|
},
|
||||||
'Cookie': this._cookieHeader()
|
body: generate(),
|
||||||
},
|
signal,
|
||||||
body: generate(),
|
bodyTimeout: UPLOAD_TIMEOUT,
|
||||||
signal,
|
headersTimeout: 60000
|
||||||
bodyTimeout: UPLOAD_TIMEOUT,
|
});
|
||||||
headersTimeout: 60000
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
// Label which phase failed so a future "fetch failed"/"terminated" is
|
|
||||||
// attributable to the big upload POST vs the small bookend requests. The
|
|
||||||
// original message is preserved as a substring so upload-manager's
|
|
||||||
// transient classification still matches. NOTE: undici may surface
|
|
||||||
// "terminated"/"other side closed", which are not yet in that transient
|
|
||||||
// list — revisit if logs show them.
|
|
||||||
const mb = Math.round(bytesRead / 1048576);
|
|
||||||
throw new Error(`Doodstream Upload-POST (${mb} MB an ${uploadUrl}): ${err && err.message ? err.message : err}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusCode = uploadRes.statusCode;
|
const statusCode = uploadRes.statusCode;
|
||||||
_debugLog(`Upload response status: ${statusCode}`);
|
_debugLog(`Upload response status: ${statusCode}`);
|
||||||
@ -433,28 +405,15 @@ class DoodstreamUploader {
|
|||||||
|
|
||||||
_debugLog(`Submitting upload_result to ${BASE_URL}/ with fields: ${JSON.stringify(hiddenFields)}`);
|
_debugLog(`Submitting upload_result to ${BASE_URL}/ with fields: ${JSON.stringify(hiddenFields)}`);
|
||||||
const formData = new URLSearchParams(hiddenFields);
|
const formData = new URLSearchParams(hiddenFields);
|
||||||
let followText = '';
|
const followRes = await this._fetch(BASE_URL + '/', {
|
||||||
try {
|
method: 'POST',
|
||||||
const followRes = await this._fetch(BASE_URL + '/', {
|
headers: {
|
||||||
method: 'POST',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
headers: {
|
'Referer': BASE_URL + '/'
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
},
|
||||||
'Referer': BASE_URL + '/'
|
body: formData.toString()
|
||||||
},
|
});
|
||||||
body: formData.toString()
|
const followText = await followRes.text();
|
||||||
});
|
|
||||||
followText = await followRes.text();
|
|
||||||
} catch (err) {
|
|
||||||
// The file already uploaded to the CDN; this POST only registers it on
|
|
||||||
// doodstream's side. If it fails transiently (even after _fetch's own
|
|
||||||
// retries) but we already hold the filecode, the upload succeeded from
|
|
||||||
// the user's view — return it rather than discarding a done upload.
|
|
||||||
if (fnCode && fnCode.length >= 8) {
|
|
||||||
_debugLog(`upload_result submit failed (${err && err.message}); using fn ${fnCode}`);
|
|
||||||
return this._buildResult(fnCode);
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
_debugLog(`upload_result response (first 500): ${followText.slice(0, 500)}`);
|
_debugLog(`upload_result response (first 500): ${followText.slice(0, 500)}`);
|
||||||
|
|
||||||
// Try to find filecode in result page
|
// Try to find filecode in result page
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "multi-hoster-uploader",
|
"name": "multi-hoster-uploader",
|
||||||
"version": "3.3.27",
|
"version": "3.3.26",
|
||||||
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -64,25 +64,6 @@ test('happy path: link in result page wins', async () => {
|
|||||||
assert.equal(res.file_code, 'jjsuhr931ds9');
|
assert.equal(res.file_code, 'jjsuhr931ds9');
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- _fetch: transient network blips on the small requests self-heal ---
|
|
||||||
test('_fetch retries a transient network failure then succeeds', async () => {
|
|
||||||
const up = new DoodstreamUploader();
|
|
||||||
const origFetch = globalThis.fetch;
|
|
||||||
let calls = 0;
|
|
||||||
globalThis.fetch = async () => {
|
|
||||||
calls++;
|
|
||||||
if (calls === 1) throw new TypeError('fetch failed');
|
|
||||||
return { status: 200, headers: { getSetCookie: () => [], get: () => null }, text: async () => 'ok' };
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
const res = await up._fetch('https://example.test/x');
|
|
||||||
assert.equal(calls, 2); // failed once, retried, succeeded
|
|
||||||
assert.equal(await res.text(), 'ok');
|
|
||||||
} finally {
|
|
||||||
globalThis.fetch = origFetch;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- _getUploadServer: discovery must never fall back to a hardcoded node ---
|
// --- _getUploadServer: discovery must never fall back to a hardcoded node ---
|
||||||
function fakeRes(body, { status = 200, ctype = 'text/html' } = {}) {
|
function fakeRes(body, { status = 200, ctype = 'text/html' } = {}) {
|
||||||
return { status, headers: { get: (h) => (h.toLowerCase() === 'content-type' ? ctype : null) }, text: async () => body };
|
return { status, headers: { get: (h) => (h.toLowerCase() === 'content-type' ? ctype : null) }, text: async () => body };
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user