fix(jira): make Jira Cloud and Jira DC HTTP timeouts configurable and consistent (#15012)

This commit is contained in:
Alona
2026-06-27 00:02:46 +00:00
committed by GitHub
parent ff3f025598
commit de4e2eb265
10 changed files with 106 additions and 19 deletions
+4 -1
View File
@@ -36,6 +36,7 @@ from integrations.utils import (
get_session_expired_message,
)
from jinja2 import Environment, FileSystemLoader
from server.auth.constants import JIRA_HTTP_TIMEOUT
from server.auth.saas_user_auth import get_user_auth_from_keycloak_id
from server.auth.token_manager import TokenManager
from storage.jira_integration_store import JiraIntegrationStore
@@ -343,7 +344,9 @@ class JiraManager(Manager[JiraViewInterface]):
f'{JIRA_CLOUD_API_URL}/{jira_cloud_id}/rest/api/2/issue/{issue_key}/comment'
)
data = format_jira_comment_body(message)
async with httpx.AsyncClient(verify=httpx_verify_option()) as client:
async with httpx.AsyncClient(
verify=httpx_verify_option(), timeout=JIRA_HTTP_TIMEOUT
) as client:
response = await client.post(
url, auth=(svc_acc_email, svc_acc_api_key), json=data
)
@@ -5,6 +5,7 @@ from uuid import UUID
import httpx
from integrations.utils import format_jira_comment_body, get_summary_instruction
from pydantic import Field
from server.auth.constants import JIRA_HTTP_TIMEOUT
from openhands.agent_server.models import AskAgentRequest, AskAgentResponse
from openhands.app_server.event_callback.event_callback_models import (
@@ -173,7 +174,7 @@ class JiraV1CallbackProcessor(EventCallbackProcessor):
url,
json=payload,
headers=headers,
timeout=30.0,
timeout=JIRA_HTTP_TIMEOUT,
)
response.raise_for_status()
@@ -201,7 +202,9 @@ class JiraV1CallbackProcessor(EventCallbackProcessor):
raise Exception(f'Failed to send message to agent server: {error_detail}')
except httpx.TimeoutException:
error_detail = f'Request timeout after 30 seconds to {url}'
error_detail = (
f'Request timeout after {JIRA_HTTP_TIMEOUT:g} seconds to {url}'
)
_logger.exception(
'[Jira] Timeout error: %s. Request payload: %s',
error_detail,
@@ -232,7 +235,9 @@ class JiraV1CallbackProcessor(EventCallbackProcessor):
message = f'OpenHands resolved this issue:\n\n{summary}'
comment_body = format_jira_comment_body(message)
async with httpx.AsyncClient(verify=httpx_verify_option()) as client:
async with httpx.AsyncClient(
verify=httpx_verify_option(), timeout=JIRA_HTTP_TIMEOUT
) as client:
response = await client.post(
comment_url,
auth=(self.svc_acc_email, self.decrypted_api_key),
+4 -1
View File
@@ -26,6 +26,7 @@ from integrations.utils import (
infer_repo_from_message,
)
from jinja2 import Environment
from server.auth.constants import JIRA_HTTP_TIMEOUT
from storage.jira_conversation import JiraConversation
from storage.jira_integration_store import JiraIntegrationStore
from storage.jira_user import JiraUser
@@ -90,7 +91,9 @@ class JiraNewConversationView(JiraViewInterface):
try:
url = f'{JIRA_CLOUD_API_URL}/{self.jira_workspace.jira_cloud_id}/rest/api/2/issue/{self.payload.issue_key}'
async with httpx.AsyncClient(verify=httpx_verify_option()) as client:
async with httpx.AsyncClient(
verify=httpx_verify_option(), timeout=JIRA_HTTP_TIMEOUT
) as client:
response = await client.get(
url,
auth=(
@@ -30,7 +30,7 @@ from integrations.utils import (
markdown_to_jira_markup,
)
from jinja2 import Environment, FileSystemLoader
from server.auth.constants import JIRA_DC_ENABLE_OAUTH
from server.auth.constants import JIRA_DC_ENABLE_OAUTH, JIRA_DC_HTTP_TIMEOUT
from server.auth.saas_user_auth import get_user_auth_from_keycloak_id
from server.auth.token_manager import TokenManager
from storage.jira_dc_integration_store import JiraDcIntegrationStore
@@ -635,7 +635,9 @@ class JiraDcManager(Manager[JiraDcViewInterface]):
"""Return the service account's Jira (name, key) from /myself."""
url = f'{base_api_url}/rest/api/2/myself'
headers = {'Authorization': f'Bearer {svc_acc_api_key}'}
async with httpx.AsyncClient(verify=httpx_verify_option()) as client:
async with httpx.AsyncClient(
verify=httpx_verify_option(), timeout=JIRA_DC_HTTP_TIMEOUT
) as client:
response = await client.get(url, headers=headers)
response.raise_for_status()
data = response.json()
@@ -647,7 +649,9 @@ class JiraDcManager(Manager[JiraDcViewInterface]):
"""Get issue details from Jira DC API."""
url = f'{job_context.base_api_url}/rest/api/2/issue/{job_context.issue_key}'
headers = {'Authorization': f'Bearer {svc_acc_api_key}'}
async with httpx.AsyncClient(verify=httpx_verify_option()) as client:
async with httpx.AsyncClient(
verify=httpx_verify_option(), timeout=JIRA_DC_HTTP_TIMEOUT
) as client:
response = await client.get(url, headers=headers)
if response.status_code == 401:
logger.error(
@@ -712,7 +716,9 @@ class JiraDcManager(Manager[JiraDcViewInterface]):
'maxResults': max_comments,
}
try:
async with httpx.AsyncClient(verify=httpx_verify_option()) as client:
async with httpx.AsyncClient(
verify=httpx_verify_option(), timeout=JIRA_DC_HTTP_TIMEOUT
) as client:
response = await client.get(url, headers=headers, params=params)
response.raise_for_status()
raw_comments = response.json().get('comments', [])
@@ -765,7 +771,9 @@ class JiraDcManager(Manager[JiraDcViewInterface]):
headers = {'Authorization': f'Bearer {svc_acc_api_key}'}
# Convert standard Markdown to Jira Wiki Markup for proper rendering
data = {'body': markdown_to_jira_markup(message)}
async with httpx.AsyncClient(verify=httpx_verify_option()) as client:
async with httpx.AsyncClient(
verify=httpx_verify_option(), timeout=JIRA_DC_HTTP_TIMEOUT
) as client:
response = await client.post(url, headers=headers, json=data)
response.raise_for_status()
return response.json()
@@ -791,7 +799,9 @@ class JiraDcManager(Manager[JiraDcViewInterface]):
url = f'{base_api_url}/rest/internal/2/reactions'
headers = {'Authorization': f'Bearer {svc_acc_api_key}'}
data = {'commentId': comment_id, 'emojiId': emoji_id}
async with httpx.AsyncClient(verify=httpx_verify_option()) as client:
async with httpx.AsyncClient(
verify=httpx_verify_option(), timeout=JIRA_DC_HTTP_TIMEOUT
) as client:
response = await client.post(url, headers=headers, json=data)
response.raise_for_status()
@@ -858,7 +868,9 @@ class JiraDcManager(Manager[JiraDcViewInterface]):
'configuration': {'SECRET': secret, 'EXCLUDE_BODY': 'false'},
}
async with httpx.AsyncClient(verify=httpx_verify_option()) as client:
async with httpx.AsyncClient(
verify=httpx_verify_option(), timeout=JIRA_DC_HTTP_TIMEOUT
) as client:
# Idempotency: reuse any existing webhook already pointing at our URL.
listing = await client.get(collection_url, headers=headers)
listing.raise_for_status()
@@ -911,7 +923,9 @@ class JiraDcManager(Manager[JiraDcViewInterface]):
collection_url = f'{base}/rest/jira-webhook/1.0/webhooks'
headers = {'Authorization': f'Bearer {admin_api_key}'}
async with httpx.AsyncClient(verify=httpx_verify_option()) as client:
async with httpx.AsyncClient(
verify=httpx_verify_option(), timeout=JIRA_DC_HTTP_TIMEOUT
) as client:
listing = await client.get(collection_url, headers=headers)
listing.raise_for_status()
existing = next(
@@ -10,6 +10,7 @@ from server.auth.constants import (
JIRA_DC_BASE_URL,
JIRA_DC_CLIENT_ID,
JIRA_DC_CLIENT_SECRET,
JIRA_DC_HTTP_TIMEOUT,
)
from server.auth.token_manager import TokenManager
from storage.jira_dc_integration_store import JiraDcIntegrationStore
@@ -81,7 +82,7 @@ async def get_user_jira_dc_token(
refresh_token = token_manager.decrypt_text(enc_refresh)
try:
async with httpx.AsyncClient(
verify=httpx_verify_option(), timeout=30.0
verify=httpx_verify_option(), timeout=JIRA_DC_HTTP_TIMEOUT
) as client:
response = await client.post(
JIRA_DC_TOKEN_URL,
@@ -14,6 +14,7 @@ from integrations.jira_dc.jira_dc_service_account import (
)
from integrations.utils import get_summary_instruction, markdown_to_jira_markup
from pydantic import Field
from server.auth.constants import JIRA_DC_HTTP_TIMEOUT
from server.auth.token_manager import TokenManager
from storage.jira_dc_integration_store import JiraDcIntegrationStore
@@ -183,7 +184,7 @@ class JiraDcV1CallbackProcessor(EventCallbackProcessor):
url,
json=payload,
headers=headers,
timeout=30.0,
timeout=JIRA_DC_HTTP_TIMEOUT,
)
response.raise_for_status()
@@ -211,7 +212,9 @@ class JiraDcV1CallbackProcessor(EventCallbackProcessor):
raise Exception(f'Failed to send message to agent server: {error_detail}')
except httpx.TimeoutException:
error_detail = f'Request timeout after 30 seconds to {url}'
error_detail = (
f'Request timeout after {JIRA_DC_HTTP_TIMEOUT:g} seconds to {url}'
)
_logger.exception(
'[Jira DC] Timeout error: %s. Request payload: %s',
error_detail,
@@ -253,7 +256,9 @@ class JiraDcV1CallbackProcessor(EventCallbackProcessor):
headers = {'Authorization': f'Bearer {service_account.api_key}'}
async with httpx.AsyncClient(verify=httpx_verify_option()) as client:
async with httpx.AsyncClient(
verify=httpx_verify_option(), timeout=JIRA_DC_HTTP_TIMEOUT
) as client:
response = await client.post(
comment_url,
headers=headers,
@@ -18,7 +18,7 @@ from integrations.resolver_context import ResolverUserContext
from integrations.resolver_org_router import resolve_org_for_repo
from integrations.utils import CONVERSATION_URL
from jinja2 import Environment
from server.auth.constants import JIRA_DC_ENABLE_OAUTH
from server.auth.constants import JIRA_DC_ENABLE_OAUTH, JIRA_DC_HTTP_TIMEOUT
from storage.jira_dc_conversation import JiraDcConversation
from storage.jira_dc_integration_store import JiraDcIntegrationStore
from storage.jira_dc_user import JiraDcUser
@@ -321,7 +321,7 @@ class JiraDcExistingConversationView(JiraDcViewInterface):
url,
json=payload,
headers=headers,
timeout=30.0,
timeout=JIRA_DC_HTTP_TIMEOUT,
)
response.raise_for_status()
logger.info(
+4
View File
@@ -39,6 +39,8 @@ ENABLE_LINEAR = os.environ.get('ENABLE_LINEAR', 'false') == 'true'
ENABLE_AUTOMATIONS = os.environ.get('ENABLE_AUTOMATIONS', 'true') == 'true'
JIRA_CLIENT_ID = os.getenv('JIRA_CLIENT_ID', '').strip()
JIRA_CLIENT_SECRET = os.getenv('JIRA_CLIENT_SECRET', '').strip()
# Timeout (s) for server-side calls to Jira Cloud; configurable, mirrors Jira DC.
JIRA_HTTP_TIMEOUT = float(os.getenv('JIRA_HTTP_TIMEOUT', '30'))
LINEAR_CLIENT_ID = os.getenv('LINEAR_CLIENT_ID', '').strip()
LINEAR_CLIENT_SECRET = os.getenv('LINEAR_CLIENT_SECRET', '').strip()
JIRA_DC_CLIENT_ID = os.getenv('JIRA_DC_CLIENT_ID', '').strip()
@@ -47,6 +49,8 @@ JIRA_DC_BASE_URL = os.getenv('JIRA_DC_BASE_URL', '').strip()
JIRA_DC_ENABLE_OAUTH = os.getenv('JIRA_DC_ENABLE_OAUTH', '1') in ('1', 'true')
JIRA_DC_SERVICE_ACCOUNT_EMAIL = os.getenv('JIRA_DC_SERVICE_ACCOUNT_EMAIL', '').strip()
JIRA_DC_SERVICE_ACCOUNT_PAT = os.getenv('JIRA_DC_SERVICE_ACCOUNT_PAT', '').strip()
# Timeout (s) for server-side calls to Jira DC; configurable for slow on-prem.
JIRA_DC_HTTP_TIMEOUT = float(os.getenv('JIRA_DC_HTTP_TIMEOUT', '30'))
AUTH_URL = os.getenv('AUTH_URL', '').rstrip('/')
ROLE_CHECK_ENABLED = os.getenv('ROLE_CHECK_ENABLED', 'false').lower() in (
'1',
@@ -280,6 +280,26 @@ class TestSendMessage:
assert result == {'id': 'comment_id'}
mock_response.raise_for_status.assert_called_once()
@pytest.mark.asyncio
async def test_send_message_uses_configured_timeout(self, jira_manager):
"""Server-side Jira Cloud calls use the configured timeout, not httpx's 5s default."""
from server.auth.constants import JIRA_HTTP_TIMEOUT
mock_response = MagicMock()
mock_response.json.return_value = {'id': 'comment_id'}
mock_response.raise_for_status = MagicMock()
with patch('httpx.AsyncClient') as mock_client:
mock_client.return_value.__aenter__.return_value.post = AsyncMock(
return_value=mock_response
)
await jira_manager.send_message(
'Test message', 'PROJ-123', 'cloud-123', 'service@test.com', 'api_key'
)
assert mock_client.call_args.kwargs['timeout'] == JIRA_HTTP_TIMEOUT
class TestSendErrorFromPayload:
"""Test error comment sending from payload."""
@@ -1819,3 +1819,35 @@ class TestAddAcknowledgementReaction:
await jira_dc_manager._add_acknowledgement_reaction(
sample_job_context, sample_jira_dc_workspace
)
class TestJiraDcHttpTimeout:
"""Server-side Jira DC calls use the configurable timeout, not httpx's 5s default."""
def test_default_timeout_is_30s(self):
from server.auth.constants import JIRA_DC_HTTP_TIMEOUT
assert JIRA_DC_HTTP_TIMEOUT == 30.0
@pytest.mark.asyncio
async def test_service_account_call_uses_configured_timeout(self, jira_dc_manager):
from server.auth.constants import JIRA_DC_HTTP_TIMEOUT
resp = MagicMock()
resp.raise_for_status = MagicMock()
resp.json = MagicMock(return_value={'name': 'openhands', 'key': 'JIRAUSER1'})
client = AsyncMock()
client.get = AsyncMock(return_value=resp)
cm = MagicMock()
cm.__aenter__ = AsyncMock(return_value=client)
cm.__aexit__ = AsyncMock(return_value=False)
with patch(
'integrations.jira_dc.jira_dc_manager.httpx.AsyncClient', return_value=cm
) as mock_client:
name, key = await jira_dc_manager._fetch_service_account_identity(
'https://jira.example.com', 'svc-pat'
)
assert (name, key) == ('openhands', 'JIRAUSER1')
assert mock_client.call_args.kwargs['timeout'] == JIRA_DC_HTTP_TIMEOUT