Files
Epicure/apps/web/lib/gitea.ts
T
Arnaud e6a5e1e6ab fix: surface real network cause on Gitea issue creation failure (v0.51.4)
A thrown fetch error (DNS failure, connection refused, timeout) never
reached the res.ok branch, so its actual cause was lost — err.message
alone is often just "fetch failed" with the real reason nested in
err.cause. Now logs the full cause server-side and folds it into the
error returned to the admin support view's tooltip.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 11:20:11 +02:00

65 lines
2.4 KiB
TypeScript

import { getSiteSetting } from "@/lib/site-settings";
const LABELS: Record<string, string[]> = {
bug: ["bug"],
suggestion: ["enhancement"],
question: ["question"],
};
/**
* Opens an issue on the configured Gitea repo for a support ticket. Returns
* the issue's HTML URL on success, or null if Gitea isn't configured or the
* request fails — callers must treat this as best-effort, not block on it,
* since the ticket itself is already saved regardless.
*/
export async function createGiteaIssue(opts: {
type: string;
title: string;
body: string;
}): Promise<{ url: string | null; error: string | null }> {
const [baseUrl, token, repo] = await Promise.all([
getSiteSetting("GITEA_URL"),
getSiteSetting("GITEA_TOKEN"),
getSiteSetting("GITEA_REPO"),
]);
if (!baseUrl || !token || !repo) {
return { url: null, error: null };
}
try {
const res = await fetch(`${baseUrl.replace(/\/$/, "")}/api/v1/repos/${repo}/issues`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `token ${token}`,
},
body: JSON.stringify({
title: opts.title,
body: opts.body,
labels: LABELS[opts.type] ?? [],
}),
signal: AbortSignal.timeout(10000),
});
if (!res.ok) {
const text = await res.text().catch(() => "");
return { url: null, error: `Gitea returned ${res.status}: ${text.slice(0, 300)}` };
}
const issue = (await res.json()) as { html_url?: string };
return { url: issue.html_url ?? null, error: issue.html_url ? null : "Gitea response had no html_url" };
} catch (err) {
// A thrown fetch error (DNS failure, connection refused, timeout, TLS
// error) never reaches the res.ok branch above, so its real cause would
// otherwise be lost — err.message alone is often just "fetch failed"
// with the actual reason nested in err.cause. Log server-side with the
// full cause chain, and fold a short form into the returned message so
// it also shows up in the admin support view's tooltip, not just logs.
const cause = err instanceof Error && err.cause instanceof Error ? err.cause.message : undefined;
const message = err instanceof Error ? err.message : "Unknown error";
console.error("[gitea] createGiteaIssue failed", { baseUrl, repo, message, cause });
return { url: null, error: cause ? `${message}: ${cause}` : message };
}
}