diff --git a/sidecar/upstream.ts b/sidecar/upstream.ts index 0b269d2..cfab779 100644 --- a/sidecar/upstream.ts +++ b/sidecar/upstream.ts @@ -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 { - 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 {