diff --git a/backend-services/services/gateway_service.py b/backend-services/services/gateway_service.py index 7783600..aed2a83 100644 --- a/backend-services/services/gateway_service.py +++ b/backend-services/services/gateway_service.py @@ -368,6 +368,20 @@ class GatewayService: if api.get('api_credits_enabled') and username and not bool(api.get('api_public')): if not await credit_util.deduct_credit(api.get('api_credit_group'), username): return GatewayService.error_response(request_id, 'GTW008', 'User does not have any credits', status=401) + else: + # Recursive call with url present; re-derive API context for headers/validation + try: + parts = [p for p in (path or '').split('/') if p] + api_name_version = '' + endpoint_uri = '' + if len(parts) >= 3: + api_name_version = f'/{parts[0]}/{parts[1]}' + endpoint_uri = '/' + '/'.join(parts[2:]) + api_key = doorman_cache.get_cache('api_id_cache', api_name_version) + api = await api_util.get_api(api_key, api_name_version) + except Exception: + api = None + endpoint_uri = '' current_time = time.time() * 1000 query_params = getattr(request, 'query_params', {}) incoming_content_type = request.headers.get('Content-Type') or 'application/xml' @@ -377,7 +391,7 @@ class GatewayService: content_type = incoming_content_type else: content_type = 'text/xml; charset=utf-8' - allowed_headers = api.get('api_allowed_headers') or [] + allowed_headers = api.get('api_allowed_headers') or [] if api else [] headers = await get_headers(request, allowed_headers) headers['Content-Type'] = content_type if 'SOAPAction' not in headers: @@ -401,7 +415,7 @@ class GatewayService: pass try: - endpoint_doc = await api_util.get_endpoint(api, 'POST', '/' + endpoint_uri.lstrip('/')) + endpoint_doc = await api_util.get_endpoint(api, 'POST', '/' + endpoint_uri.lstrip('/')) if api else None endpoint_id = endpoint_doc.get('endpoint_id') if endpoint_doc else None if endpoint_id: await validation_util.validate_soap_request(endpoint_id, envelope) diff --git a/backend-services/tests/test_soap_gateway_retries.py b/backend-services/tests/test_soap_gateway_retries.py new file mode 100644 index 0000000..1d13bc2 --- /dev/null +++ b/backend-services/tests/test_soap_gateway_retries.py @@ -0,0 +1,127 @@ +import pytest + + +class _Resp: + def __init__(self, status_code=200, body='', headers=None): + self.status_code = status_code + self.text = body + base = {'Content-Type': 'text/xml'} + if headers: + base.update(headers) + self.headers = base + self.content = (self.text or '').encode('utf-8') + + +def _mk_retry_xml_client(sequence, seen): + counter = {'i': 0} + + class _Client: + def __init__(self, timeout=None): + pass + async def __aenter__(self): + return self + async def __aexit__(self, exc_type, exc, tb): + return False + async def post(self, url, content=None, params=None, headers=None): + seen.append({'url': url, 'params': dict(params or {}), 'headers': dict(headers or {}), 'content': content}) + idx = min(counter['i'], len(sequence) - 1) + code = sequence[idx] + counter['i'] = counter['i'] + 1 + return _Resp(code) + return _Client + + +async def _setup_soap(client, name, ver, retry_count=0): + payload = { + 'api_name': name, + 'api_version': ver, + 'api_description': f'{name} {ver}', + 'api_allowed_roles': ['admin'], + 'api_allowed_groups': ['ALL'], + 'api_servers': ['http://soap.retry'], + 'api_type': 'REST', + 'api_allowed_retry_count': retry_count, + } + r = await client.post('/platform/api', json=payload) + assert r.status_code in (200, 201) + r2 = await client.post('/platform/endpoint', json={ + 'api_name': name, + 'api_version': ver, + 'endpoint_method': 'POST', + 'endpoint_uri': '/call', + 'endpoint_description': 'soap call', + }) + assert r2.status_code in (200, 201) + from conftest import subscribe_self + await subscribe_self(client, name, ver) + + +@pytest.mark.asyncio +async def test_soap_retry_on_500_then_success(monkeypatch, authed_client): + import services.gateway_service as gs + name, ver = 'soapretry500', 'v1' + await _setup_soap(authed_client, name, ver, retry_count=2) + seen = [] + monkeypatch.setattr(gs.httpx, 'AsyncClient', _mk_retry_xml_client([500, 200], seen)) + r = await authed_client.post( + f'/api/soap/{name}/{ver}/call', headers={'Content-Type': 'application/xml'}, content='' + ) + assert r.status_code == 200 + assert len(seen) == 2 + + +@pytest.mark.asyncio +async def test_soap_retry_on_502_then_success(monkeypatch, authed_client): + import services.gateway_service as gs + name, ver = 'soapretry502', 'v1' + await _setup_soap(authed_client, name, ver, retry_count=2) + seen = [] + monkeypatch.setattr(gs.httpx, 'AsyncClient', _mk_retry_xml_client([502, 200], seen)) + r = await authed_client.post( + f'/api/soap/{name}/{ver}/call', headers={'Content-Type': 'application/xml'}, content='' + ) + assert r.status_code == 200 + assert len(seen) == 2 + + +@pytest.mark.asyncio +async def test_soap_retry_on_503_then_success(monkeypatch, authed_client): + import services.gateway_service as gs + name, ver = 'soapretry503', 'v1' + await _setup_soap(authed_client, name, ver, retry_count=2) + seen = [] + monkeypatch.setattr(gs.httpx, 'AsyncClient', _mk_retry_xml_client([503, 200], seen)) + r = await authed_client.post( + f'/api/soap/{name}/{ver}/call', headers={'Content-Type': 'application/xml'}, content='' + ) + assert r.status_code == 200 + assert len(seen) == 2 + + +@pytest.mark.asyncio +async def test_soap_retry_on_504_then_success(monkeypatch, authed_client): + import services.gateway_service as gs + name, ver = 'soapretry504', 'v1' + await _setup_soap(authed_client, name, ver, retry_count=2) + seen = [] + monkeypatch.setattr(gs.httpx, 'AsyncClient', _mk_retry_xml_client([504, 200], seen)) + r = await authed_client.post( + f'/api/soap/{name}/{ver}/call', headers={'Content-Type': 'application/xml'}, content='' + ) + assert r.status_code == 200 + assert len(seen) == 2 + + +@pytest.mark.asyncio +async def test_soap_no_retry_when_retry_count_zero(monkeypatch, authed_client): + import services.gateway_service as gs + name, ver = 'soapretry0', 'v1' + await _setup_soap(authed_client, name, ver, retry_count=0) + seen = [] + monkeypatch.setattr(gs.httpx, 'AsyncClient', _mk_retry_xml_client([500, 200], seen)) + r = await authed_client.post( + f'/api/soap/{name}/{ver}/call', headers={'Content-Type': 'application/xml'}, content='' + ) + assert r.status_code == 500 + assert len(seen) == 1 +