diff --git a/lib/hosters.js b/lib/hosters.js index c4ac3c3..73b8a8d 100644 --- a/lib/hosters.js +++ b/lib/hosters.js @@ -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.`); } - throw new Error( - payload.msg - || payload.message - || `Upload zu ${hosterName} lieferte keine verwendbaren Dateidaten zurueck.` - ); + // 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( + `Upload zu ${hosterName} lieferte keine file_code-Antwort (Payload: ${snippet})` + ); + } + throw new Error(msg); } module.exports = { diff --git a/lib/upload-manager.js b/lib/upload-manager.js index 68eac54..9abbaa3 100644 --- a/lib/upload-manager.js +++ b/lib/upload-manager.js @@ -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);