fix(jira): always return HTTP 200 from jira-test + strip trailing slash
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
- jira-test now returns {status: "ok"|"error", message: ...} with
HTTP 200 so Cloudflare never intercepts the response
- jira_service strips trailing slash from URL before creating Jira
client (avoids double-slash in REST paths)
- Frontend reads data.status field instead of HTTP status code
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -295,8 +295,11 @@ def test_jira_connection(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
jira = get_user_jira_client(current_user, db)
|
jira = get_user_jira_client(current_user, db)
|
||||||
# Lightweight call: get current user info (10 s hard timeout)
|
# 10-second timeout so we never block Cloudflare into a 524
|
||||||
|
try:
|
||||||
jira._session.timeout = 10 # type: ignore[attr-defined]
|
jira._session.timeout = 10 # type: ignore[attr-defined]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
myself = jira.myself()
|
myself = jira.myself()
|
||||||
return {
|
return {
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
@@ -305,21 +308,26 @@ def test_jira_connection(
|
|||||||
}
|
}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
err = str(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:
|
if "Expecting value" in err or "line 1 column 1" in err:
|
||||||
detail = (
|
msg = (
|
||||||
"Jira returned an unexpected response — check that the URL is correct "
|
"Jira returned a non-JSON response. "
|
||||||
"and that the account email + API token are valid."
|
"Verify the URL (e.g. https://company.atlassian.net), "
|
||||||
|
"email and API token."
|
||||||
)
|
)
|
||||||
elif "401" in err or "Unauthorized" in err:
|
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:
|
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():
|
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:
|
else:
|
||||||
detail = f"Jira connection failed: {err}"
|
msg = f"Jira connection failed: {err}"
|
||||||
raise HTTPException(status_code=502, detail=detail)
|
logger.warning("Jira test connection failed: %s", err)
|
||||||
|
return {"status": "error", "message": msg, "jira_url": jira_url}
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -142,8 +142,12 @@ def get_user_jira_client(user: User, db: Session):
|
|||||||
|
|
||||||
from atlassian import Jira
|
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(
|
return Jira(
|
||||||
url=jira_url,
|
url=clean_url,
|
||||||
username=auth_email,
|
username=auth_email,
|
||||||
password=user.jira_api_token,
|
password=user.jira_api_token,
|
||||||
cloud=True,
|
cloud=True,
|
||||||
|
|||||||
@@ -178,7 +178,12 @@ export async function updateJiraConfig(payload: JiraConfigUpdate): Promise<JiraC
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function testJiraConnection(): Promise<{ status: string; connected_as: string; jira_url: string }> {
|
export async function testJiraConnection(): Promise<{
|
||||||
|
status: "ok" | "error";
|
||||||
|
connected_as?: string;
|
||||||
|
jira_url?: string;
|
||||||
|
message?: string;
|
||||||
|
}> {
|
||||||
const { data } = await client.post("/system/jira-test");
|
const { data } = await client.post("/system/jira-test");
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1046,8 +1046,14 @@ function JiraConfigSection() {
|
|||||||
const testMut = useMutation({
|
const testMut = useMutation({
|
||||||
mutationFn: testJiraConnection,
|
mutationFn: testJiraConnection,
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
setTestResult({ connectedAs: data.connected_as, url: data.jira_url });
|
// 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);
|
setTestError(null);
|
||||||
|
} else {
|
||||||
|
setTestError((data as { message?: string }).message ?? "Connection failed");
|
||||||
|
setTestResult(null);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onError: (err: Error) => {
|
onError: (err: Error) => {
|
||||||
setTestError(err.message || "Connection failed");
|
setTestError(err.message || "Connection failed");
|
||||||
|
|||||||
Reference in New Issue
Block a user