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>
This commit is contained in:
@@ -125,6 +125,7 @@ CI workflow **Sync release to Gitea** (`.github/workflows/gitea-release-sync.yml
|
|||||||
6. **Secrets** — **`GITEA_BASE_URL`**, **`GITEA_TOKEN`**, **`GITEA_OWNER`**, **`GITEA_REPO`** must be set under **Settings → Secrets and variables → Actions**. A failed “Upload to Gitea” step usually prints which is missing.
|
6. **Secrets** — **`GITEA_BASE_URL`**, **`GITEA_TOKEN`**, **`GITEA_OWNER`**, **`GITEA_REPO`** must be set under **Settings → Secrets and variables → Actions**. A failed “Upload to Gitea” step usually prints which is missing.
|
||||||
7. **Actions tab** — Open the latest **Sync release to Gitea** run; a red **build-electron** (old tag without `package-lock.json`, etc.) or **Upload to Gitea** step shows the real error.
|
7. **Actions tab** — Open the latest **Sync release to Gitea** run; a red **build-electron** (old tag without `package-lock.json`, etc.) or **Upload to Gitea** step shows the real error.
|
||||||
8. **HTTP 422 `repo is empty`** — The Gitea repo has **no commits** yet. Push any initial commit (e.g. **Add README** in the Gitea web UI, or `git push` to **`main`**). Optionally set **`GITEA_TARGET_REF`** to match your real default branch if it is not **`main`**. From this repo you can run **`scripts/bootstrap-gitea-repo.sh`** (see script header for `GITEA_*` env or pass the HTTPS/SSH clone URL as the first argument).
|
8. **HTTP 422 `repo is empty`** — The Gitea repo has **no commits** yet. Push any initial commit (e.g. **Add README** in the Gitea web UI, or `git push` to **`main`**). Optionally set **`GITEA_TARGET_REF`** to match your real default branch if it is not **`main`**. From this repo you can run **`scripts/bootstrap-gitea-repo.sh`** (see script header for `GITEA_*` env or pass the HTTPS/SSH clone URL as the first argument).
|
||||||
|
9. **`sync Wow.exe: fetch failed`** (Linux/AppImage especially) — Usually **TLS** (self-signed Gitea cert) or **cannot reach** the host. Use a **trusted certificate** on Gitea, or put your CA in the system trust store, or set **`NODE_EXTRA_CA_CERTS=/path/to/ca.pem`** when launching the AppImage if you must use self-signed HTTPS. If the mirror is LAN-only HTTP, set **`base_url`** in **`lib/baked-gitea-channel.js`** to **`http://…`** (only if acceptable). Ensure **`Wow-patched.exe`** exists on the **same Gitea release** you configured (**`release_tag`**: `latest` vs pinned tag). Newer builds show the failing URL and TLS hints in the error text.
|
||||||
|
|
||||||
### Private Gitea token for players
|
### Private Gitea token for players
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { downloadBodyToFile } = require('./http-download');
|
const { downloadBodyToFile, fetchOrThrow } = require('./http-download');
|
||||||
|
|
||||||
function normalizeGiteaBaseUrl(raw) {
|
function normalizeGiteaBaseUrl(raw) {
|
||||||
let b = String(raw || '').trim().replace(/\/+$/, '');
|
let b = String(raw || '').trim().replace(/\/+$/, '');
|
||||||
@@ -46,7 +46,7 @@ async function downloadGiteaReleaseAsset(cfg, assetName, destPath) {
|
|||||||
listUrl = `${api}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/tags/${encodeURIComponent(tag)}`;
|
listUrl = `${api}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/tags/${encodeURIComponent(tag)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(listUrl, { headers: giteaHeaders(token, true) });
|
const res = await fetchOrThrow(listUrl, { headers: giteaHeaders(token, true) });
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
let hint = '';
|
let hint = '';
|
||||||
@@ -69,7 +69,7 @@ async function downloadGiteaReleaseAsset(cfg, assetName, destPath) {
|
|||||||
|
|
||||||
const h = { Accept: 'application/octet-stream' };
|
const h = { Accept: 'application/octet-stream' };
|
||||||
if (token) h.Authorization = `token ${token}`;
|
if (token) h.Authorization = `token ${token}`;
|
||||||
const dl = await fetch(downloadUrl, { headers: h, redirect: 'follow' });
|
const dl = await fetchOrThrow(downloadUrl, { headers: h, redirect: 'follow' });
|
||||||
await downloadBodyToFile(dl, destPath);
|
await downloadBodyToFile(dl, destPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ async function getGiteaUpdaterFeedBase(cfg) {
|
|||||||
} else {
|
} else {
|
||||||
listUrl = `${api}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/tags/${encodeURIComponent(tag)}`;
|
listUrl = `${api}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/tags/${encodeURIComponent(tag)}`;
|
||||||
}
|
}
|
||||||
const res = await fetch(listUrl, { headers: giteaHeaders(token, true) });
|
const res = await fetchOrThrow(listUrl, { headers: giteaHeaders(token, true) });
|
||||||
if (!res.ok) return null;
|
if (!res.ok) return null;
|
||||||
const rel = await res.json();
|
const rel = await res.json();
|
||||||
const tagName = rel.tag_name;
|
const tagName = rel.tag_name;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const path = require('path');
|
|||||||
const fs = require('fs').promises;
|
const fs = require('fs').promises;
|
||||||
const { githubToken } = require('./github-token');
|
const { githubToken } = require('./github-token');
|
||||||
const { downloadGiteaReleaseAsset, useGiteaReleases } = require('./gitea-release');
|
const { downloadGiteaReleaseAsset, useGiteaReleases } = require('./gitea-release');
|
||||||
const { fetchToFile, downloadBodyToFile } = require('./http-download');
|
const { fetchToFile, downloadBodyToFile, fetchOrThrow } = require('./http-download');
|
||||||
|
|
||||||
function encodeRepoPath(repoPath) {
|
function encodeRepoPath(repoPath) {
|
||||||
let p = String(repoPath || '').replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
let p = String(repoPath || '').replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
||||||
@@ -35,7 +35,7 @@ async function downloadGitHubRepoFile(cfg, repoPath, destPath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const apiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${enc}?ref=${encodeURIComponent(ref)}`;
|
const apiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${enc}?ref=${encodeURIComponent(ref)}`;
|
||||||
const res = await fetch(apiUrl, { headers: ghHeaders(token, true) });
|
const res = await fetchOrThrow(apiUrl, { headers: ghHeaders(token, true) });
|
||||||
const body = await res.text();
|
const body = await res.text();
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(`GitHub contents API ${res.status}: ${body.slice(0, 800)}`);
|
throw new Error(`GitHub contents API ${res.status}: ${body.slice(0, 800)}`);
|
||||||
@@ -78,7 +78,7 @@ async function downloadReleaseAsset(cfg, assetName, destPath) {
|
|||||||
} else {
|
} else {
|
||||||
listUrl = `https://api.github.com/repos/${owner}/${repo}/releases/tags/${encodeURIComponent(tag)}`;
|
listUrl = `https://api.github.com/repos/${owner}/${repo}/releases/tags/${encodeURIComponent(tag)}`;
|
||||||
}
|
}
|
||||||
const res = await fetch(listUrl, { headers: ghHeaders(token, true) });
|
const res = await fetchOrThrow(listUrl, { headers: ghHeaders(token, true) });
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
let hint = '';
|
let hint = '';
|
||||||
@@ -114,7 +114,7 @@ async function downloadReleaseAsset(cfg, assetName, destPath) {
|
|||||||
h.Authorization = `Bearer ${token}`;
|
h.Authorization = `Bearer ${token}`;
|
||||||
h['X-GitHub-Api-Version'] = '2022-11-28';
|
h['X-GitHub-Api-Version'] = '2022-11-28';
|
||||||
}
|
}
|
||||||
const dl = await fetch(assetURL, { headers: h, redirect: 'follow' });
|
const dl = await fetchOrThrow(assetURL, { headers: h, redirect: 'follow' });
|
||||||
await downloadBodyToFile(dl, destPath);
|
await downloadBodyToFile(dl, destPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,42 @@ const { createWriteStream } = require('fs');
|
|||||||
const { pipeline } = require('stream/promises');
|
const { pipeline } = require('stream/promises');
|
||||||
const { Readable } = require('stream');
|
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) {
|
async function downloadBodyToFile(res, destPath) {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const errText = await res.text().catch(() => '');
|
const errText = await res.text().catch(() => '');
|
||||||
@@ -30,11 +66,11 @@ async function downloadBodyToFile(res, destPath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchToFile(url, headers, destPath) {
|
async function fetchToFile(url, headers, destPath) {
|
||||||
const res = await fetch(url, {
|
const res = await fetchOrThrow(url, {
|
||||||
headers,
|
headers,
|
||||||
redirect: 'follow',
|
redirect: 'follow',
|
||||||
});
|
});
|
||||||
await downloadBodyToFile(res, destPath);
|
await downloadBodyToFile(res, destPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { fetchToFile, downloadBodyToFile };
|
module.exports = { fetchToFile, downloadBodyToFile, fetchOrThrow, safeUrlForLog };
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "fractured-launcher-electron",
|
"name": "fractured-launcher-electron",
|
||||||
"version": "1.0.4",
|
"version": "1.0.5",
|
||||||
"description": "Fractured WoW launcher (Electron) — no console window, native folder picker, auto-update",
|
"description": "Fractured WoW launcher (Electron) — no console window, native folder picker, auto-update",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
Reference in New Issue
Block a user