c1f7eaa153
- fetchOrThrow wraps global fetch with TLS/DNS/refused hints + URL (sanitized). - Use in gitea-release, github paths; fetchToFile already benefits. - README checklist for sync Wow.exe fetch failed; version 1.0.5. Co-authored-by: Cursor <cursoragent@cursor.com>
77 lines
2.5 KiB
JavaScript
77 lines
2.5 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('fs').promises;
|
|
const path = require('path');
|
|
const { createWriteStream } = require('fs');
|
|
const { pipeline } = require('stream/promises');
|
|
const { Readable } = require('stream');
|
|
|
|
function safeUrlForLog(url) {
|
|
try {
|
|
const u = new URL(url);
|
|
return `${u.origin}${u.pathname}`;
|
|
} catch {
|
|
return String(url || '').split('?')[0].slice(0, 200);
|
|
}
|
|
}
|
|
|
|
function explainFetchFailure(err, url) {
|
|
const msg = err && err.message ? err.message : String(err);
|
|
const cause = err && err.cause;
|
|
const code = cause && cause.code ? cause.code : '';
|
|
const combined = `${msg} ${code}`;
|
|
const hints = [];
|
|
if (/CERT|TLS|SSL|UNABLE_TO_VERIFY|SELF_SIGNED|certificate|unknown ca|unable to verify/i.test(combined)) {
|
|
hints.push(
|
|
'TLS certificate not trusted — install a valid cert on Gitea, or trust your CA system-wide, or set NODE_EXTRA_CA_CERTS to a .pem bundle (self-signed mirrors)'
|
|
);
|
|
}
|
|
if (/ECONNREFUSED/.test(combined)) hints.push('connection refused (wrong host/port or server down)');
|
|
if (/ENOTFOUND|EAI_AGAIN/.test(combined)) hints.push('DNS lookup failed');
|
|
if (/ETIMEDOUT|TIMEOUT/i.test(combined)) hints.push('connection timed out');
|
|
const hintStr = hints.length ? ` ${hints.join(' ')}` : '';
|
|
return new Error(`${msg}${hintStr} — ${safeUrlForLog(url)}`);
|
|
}
|
|
|
|
/** Wrap global fetch with clearer errors for TLS/DNS/refused (Electron reports bare "fetch failed"). */
|
|
async function fetchOrThrow(url, init) {
|
|
try {
|
|
return await fetch(url, init);
|
|
} catch (e) {
|
|
throw explainFetchFailure(e, url);
|
|
}
|
|
}
|
|
|
|
async function downloadBodyToFile(res, destPath) {
|
|
if (!res.ok) {
|
|
const errText = await res.text().catch(() => '');
|
|
throw new Error(`HTTP ${res.status}: ${errText.slice(0, 500)}`);
|
|
}
|
|
if (!res.body) {
|
|
throw new Error('download has no body');
|
|
}
|
|
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
|
const tmp = destPath + '.downloading';
|
|
let body = res.body;
|
|
if (body && typeof body.pipe !== 'function') {
|
|
body = Readable.fromWeb(body);
|
|
}
|
|
await pipeline(body, createWriteStream(tmp));
|
|
const st = await fs.stat(tmp);
|
|
if (st.size === 0) {
|
|
await fs.unlink(tmp).catch(() => {});
|
|
throw new Error('empty download');
|
|
}
|
|
await fs.rename(tmp, destPath);
|
|
}
|
|
|
|
async function fetchToFile(url, headers, destPath) {
|
|
const res = await fetchOrThrow(url, {
|
|
headers,
|
|
redirect: 'follow',
|
|
});
|
|
await downloadBodyToFile(res, destPath);
|
|
}
|
|
|
|
module.exports = { fetchToFile, downloadBodyToFile, fetchOrThrow, safeUrlForLog };
|