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
+