From 39f2d0c7ed8316cb0b7cdb812157e620db309d11 Mon Sep 17 00:00:00 2001 From: officer Date: Wed, 19 Aug 2026 10:38:30 +0000 Subject: [PATCH] use Gitea's public health endpoint to check the URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /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. --- sidecar/upstream.ts | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) 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 {