fix(rotation): transient network errors don't blacklist the account + clearer byse 'OK' error

Two bugs visible in the user's rotation log:

  1. 'error=OK' for byse.sx — the server returned a payload with
     msg='OK' and no file_code anywhere we recognized. Our generic
     uploadFile threw the bare 'OK' as the error message, which is
     useless and misleading. Now when we see an ok-ish msg without
     the expected file_code we throw a descriptive error that
     includes the first ~400 bytes of the payload so the next time
     it happens we can see what's actually being returned (API
     changed, new field name, etc.).

  2. 'getaddrinfo ENOTFOUND s1055.filemoon' was marking accounts as
     permanently failed, blacklisting BOTH byse accounts within the
     same batch even though neither was the actual problem — filemoon
     (byse's storage backend) briefly had a DNS blip. Added
     _isTransientNetworkError() covering DNS/ECONNRESET/ETIMEDOUT/etc.
     When all retries on an account exhaust with a transient error,
     we now fail just that file and emit 'skip-rotation-transient'
     instead of adding the account to _failedAccounts. Other files
     in the same batch still get a fresh try on the same account.
This commit is contained in:
Administrator 2026-04-20 15:56:44 +02:00
parent 22869df8a5
commit 0ea92ad6d0
2 changed files with 55 additions and 7 deletions

View File

@ -413,11 +413,19 @@ async function uploadFile(hosterName, filePath, apiKey, onProgress, signal, thro
throw new Error(payload.msg || payload.message || `Upload zu ${hosterName} wurde vom Server abgelehnt.`);
}
// Avoid throwing a bare "OK" / "SUCCESS" as the error message — that happens
// when the server says "msg: OK" but ships no file_code anywhere we know
// about, typically an API change. Surface the full (trimmed) payload so
// future logs actually show what the server returned.
const msg = String(payload.msg || payload.message || '').trim();
const isOkishNoPayload = /^(ok|success|done|accepted)$/i.test(msg);
if (isOkishNoPayload || !msg) {
const snippet = JSON.stringify(payload).slice(0, 400);
throw new Error(
payload.msg
|| payload.message
|| `Upload zu ${hosterName} lieferte keine verwendbaren Dateidaten zurueck.`
`Upload zu ${hosterName} lieferte keine file_code-Antwort (Payload: ${snippet})`
);
}
throw new Error(msg);
}
module.exports = {

View File

@ -56,11 +56,38 @@ class UploadManager extends EventEmitter {
this.emit('rot-log', { ts: Date.now(), event, ...data });
}
// Transient network errors — the account is fine, the network or the
// hoster's own backend hiccuped. Retrying on the SAME account is the right
// move; marking it failed would wrongly poison the fallback chain. If all
// retries on the current account still hit this class of error, we bail
// out for this file without blacklisting the account, so other jobs in the
// batch still get a fresh chance on it.
_isTransientNetworkError(err) {
if (!err || !err.message) return false;
const m = String(err.message);
const TRANSIENT = [
/ENOTFOUND/i,
/ECONNRESET/i,
/ECONNREFUSED/i,
/ETIMEDOUT/i,
/EAI_AGAIN/i,
/EHOSTUNREACH/i,
/ENETUNREACH/i,
/EPIPE/i,
/socket hang up/i,
/network (error|failure|problem)/i,
/dns (lookup|error|failed)/i,
/getaddrinfo/i,
/fetch failed/i,
/\bconnect (ETIMEDOUT|ECONN)/i
];
return TRANSIENT.some(p => p.test(m));
}
// Error classes that mean "this account is the problem, retrying on it won't
// help" — we skip the remaining retries and go straight to the fallback
// account. Keeps single runs fast when an account is rate-limited, banned,
// or out of quota. Transient network issues still go through the normal
// retry loop on the same account.
// or out of quota.
_shouldSkipRetryOnAccountError(err) {
if (!err || !err.message) return false;
const m = String(err.message);
@ -542,6 +569,19 @@ class UploadManager extends EventEmitter {
hoster: task.hoster, fileName, accountId: task.accountId,
lastError: lastError ? lastError.message : null
});
// If the reason for failure was a transient network error we do NOT
// blacklist the account. Other jobs on the same account in this batch
// can still try fresh. This file just errors out for now.
if (this._isTransientNetworkError(lastError)) {
this._rotLog('skip-rotation-transient', {
hoster: task.hoster, fileName, accountId: task.accountId,
lastError: lastError ? lastError.message : null
});
const error = lastError.message || 'Netzwerkfehler';
emitFinalStatus('error', { error });
recordFinalResult('error', { error });
return;
}
while (task.accountId) {
if (signal.aborted || this.stopAfterActive) break;
const alreadyMarked = this._failedAccounts.has(task.hoster + ':' + task.accountId);