Files
Fractured/tools/fractured-launcher-electron/lib/http-download.js
T
Docker Build c1f7eaa153 fix(launcher): clearer fetch errors for Gitea TLS/DNS (fetch failed)
- 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>
2026-05-10 21:46:48 -05:00

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 };