diff --git a/backend/app/routers/system.py b/backend/app/routers/system.py index 72cfa86..35f0aba 100644 --- a/backend/app/routers/system.py +++ b/backend/app/routers/system.py @@ -295,8 +295,11 @@ def test_jira_connection( try: jira = get_user_jira_client(current_user, db) - # Lightweight call: get current user info (10 s hard timeout) - jira._session.timeout = 10 # type: ignore[attr-defined] + # 10-second timeout so we never block Cloudflare into a 524 + try: + jira._session.timeout = 10 # type: ignore[attr-defined] + except Exception: + pass myself = jira.myself() return { "status": "ok", @@ -305,21 +308,26 @@ def test_jira_connection( } except Exception as exc: err = str(exc) - # Translate common Atlassian-library errors into human-readable messages + # Always return HTTP 200 with status="error" so Cloudflare never + # intercepts the response and the frontend always sees our message. if "Expecting value" in err or "line 1 column 1" in err: - detail = ( - "Jira returned an unexpected response — check that the URL is correct " - "and that the account email + API token are valid." + msg = ( + "Jira returned a non-JSON response. " + "Verify the URL (e.g. https://company.atlassian.net), " + "email and API token." ) elif "401" in err or "Unauthorized" in err: - detail = "Authentication failed (401). Verify your Atlassian email and API token." + msg = "Authentication failed (401). Check your Atlassian email and API token." elif "403" in err or "Forbidden" in err: - detail = "Access denied (403). The token may not have permission to access this Jira instance." + msg = "Access denied (403). The token may not have permission for this Jira project." elif "timed out" in err.lower() or "timeout" in err.lower(): - detail = "Connection timed out. Check the Jira URL is reachable from the server." + msg = "Connection timed out. Check that the Jira URL is reachable from the server." + elif "not configured" in err.lower(): + msg = err else: - detail = f"Jira connection failed: {err}" - raise HTTPException(status_code=502, detail=detail) + msg = f"Jira connection failed: {err}" + logger.warning("Jira test connection failed: %s", err) + return {"status": "error", "message": msg, "jira_url": jira_url} # --------------------------------------------------------------------------- diff --git a/backend/app/services/jira_service.py b/backend/app/services/jira_service.py index c040a3c..2b81ebc 100644 --- a/backend/app/services/jira_service.py +++ b/backend/app/services/jira_service.py @@ -142,8 +142,12 @@ def get_user_jira_client(user: User, db: Session): from atlassian import Jira + # Strip trailing slash — the Atlassian library appends paths like + # /rest/api/2/myself and a trailing slash causes double-slash URLs. + clean_url = jira_url.rstrip("/") + return Jira( - url=jira_url, + url=clean_url, username=auth_email, password=user.jira_api_token, cloud=True, diff --git a/frontend/src/api/settings.ts b/frontend/src/api/settings.ts index ff4d185..3cf9532 100644 --- a/frontend/src/api/settings.ts +++ b/frontend/src/api/settings.ts @@ -178,7 +178,12 @@ export async function updateJiraConfig(payload: JiraConfigUpdate): Promise { +export async function testJiraConnection(): Promise<{ + status: "ok" | "error"; + connected_as?: string; + jira_url?: string; + message?: string; +}> { const { data } = await client.post("/system/jira-test"); return data; } diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index 5c5029f..0ee55ea 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -1046,8 +1046,14 @@ function JiraConfigSection() { const testMut = useMutation({ mutationFn: testJiraConnection, onSuccess: (data) => { - setTestResult({ connectedAs: data.connected_as, url: data.jira_url }); - setTestError(null); + // Backend always returns HTTP 200; status field tells us if it worked + if (data.status === "ok") { + setTestResult({ connectedAs: data.connected_as ?? "", url: data.jira_url ?? "" }); + setTestError(null); + } else { + setTestError((data as { message?: string }).message ?? "Connection failed"); + setTestResult(null); + } }, onError: (err: Error) => { setTestError(err.message || "Connection failed");