use Gitea's public health endpoint to check the URL

/api/v1/version is not reliably public — an instance with REQUIRE_SIGNIN_VIEW
answers 403 — so naming a perfectly good URL was refused with "the server
answered 403" and whoever typed it had nothing to act on.

/api/healthz is Gitea's own health endpoint and answers before any
authentication, which is the whole point of it. Tried first, with version as
the fallback.

A 401 or 403 from either now counts as reachable. This step only asks whether
there is a Gitea at the address; whether the caller may READ it is the token
step's question, and failing here for that reason sends people looking for the
wrong problem.
This commit is contained in:
officer
2026-08-19 10:38:30 +00:00
parent 8125363e0b
commit 39f2d0c7ed
+28 -7
View File
@@ -124,14 +124,35 @@ export type ProbeResult = { ok: boolean; version: string | null; user: string |
* up", which is exactly what this step needs to establish.
*/
export async function reachable(base: string): Promise<ProbeResult> {
try {
const res = await fetch(`${base}/api/v1/version`, { signal: AbortSignal.timeout(8000) });
if (!res.ok) return { ok: false, version: null, user: null, error: `the server answered ${res.status}` };
const body = (await res.json().catch(() => null)) as { version?: string } | null;
return { ok: true, version: body?.version ?? null, user: null };
} catch {
return { ok: false, version: null, user: null, error: 'could not reach that URL from the server' };
// `/api/healthz` FIRST. It is Gitea's own health endpoint and it is public by design — it answers
// before any authentication, which is the whole point of it.
//
// `/api/v1/version` was tried first and is NOT reliably public: an instance with
// `REQUIRE_SIGNIN_VIEW` answers 403, so a perfectly good URL was refused with "the server answered
// 403" and whoever typed it had no idea what to do about it.
//
// A 401 or 403 from EITHER endpoint still proves something answered at that address. This step only
// asks "is there a Gitea here"; whether the caller may read it is the token step's question, and
// failing here for that reason sends people looking for the wrong problem.
for (const path of ['/api/healthz', '/api/v1/version']) {
try {
const res = await fetch(`${base}${path}`, { signal: AbortSignal.timeout(8000) });
if (res.ok) {
const body = (await res.json().catch(() => null)) as { version?: string } | null;
return { ok: true, version: body?.version ?? null, user: null };
}
if (res.status === 401 || res.status === 403) return { ok: true, version: null, user: null };
// Anything else — 404, 502, a login page — is worth trying the other path for before giving up.
} catch {
// Network-level. Try the other path; if both fail the error below is the honest one.
}
}
return {
ok: false,
version: null,
user: null,
error: 'no Gitea answered at that URL — check it is reachable from the server, not just from your browser',
};
}
export async function probe(config: GiteaConfig): Promise<ProbeResult> {