Compare commits

..

No commits in common. "3ef3e074e6be89acb9894267936719517ffa1472" and "8f500c590e28ddb9d6203ea67ba51441ac06557e" have entirely different histories.

4 changed files with 28 additions and 89 deletions

View File

@ -25,7 +25,6 @@ export default [
URL: 'readonly',
fetch: 'readonly',
AbortController: 'readonly',
AbortSignal: 'readonly',
navigator: 'readonly',
document: 'readonly',
window: 'readonly',

View File

@ -76,27 +76,11 @@ class DoodstreamUploader {
headers['Cookie'] = this._cookieHeader();
}
// The small discovery/result requests that bookend a multi-minute upload
// occasionally hit a transient blip ("fetch failed", ECONNRESET, a hung TLS
// handshake). A blip here shouldn't throw away the whole upload, so retry a
// few times with short backoff. Each attempt gets its own 20s timeout —
// 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));
}
}
const res = await fetch(url, {
...opts,
headers,
redirect: 'manual'
});
this._parseCookiesFromHeaders(res.headers);
@ -312,9 +296,7 @@ class DoodstreamUploader {
yield epilogueBuf;
}
let uploadRes;
try {
uploadRes = await request(uploadUrl, {
const uploadRes = await request(uploadUrl, {
method: 'POST',
headers: {
'Content-Type': `multipart/form-data; boundary=${boundary}`,
@ -327,16 +309,6 @@ class DoodstreamUploader {
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;
_debugLog(`Upload response status: ${statusCode}`);
@ -433,8 +405,6 @@ class DoodstreamUploader {
_debugLog(`Submitting upload_result to ${BASE_URL}/ with fields: ${JSON.stringify(hiddenFields)}`);
const formData = new URLSearchParams(hiddenFields);
let followText = '';
try {
const followRes = await this._fetch(BASE_URL + '/', {
method: 'POST',
headers: {
@ -443,18 +413,7 @@ class DoodstreamUploader {
},
body: formData.toString()
});
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;
}
const followText = await followRes.text();
_debugLog(`upload_result response (first 500): ${followText.slice(0, 500)}`);
// Try to find filecode in result page

View File

@ -1,6 +1,6 @@
{
"name": "multi-hoster-uploader",
"version": "3.3.27",
"version": "3.3.26",
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
"main": "main.js",
"scripts": {

View File

@ -64,25 +64,6 @@ test('happy path: link in result page wins', async () => {
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 ---
function fakeRes(body, { status = 200, ctype = 'text/html' } = {}) {
return { status, headers: { get: (h) => (h.toLowerCase() === 'content-type' ? ctype : null) }, text: async () => body };