fix(api): Add automatic token refresh on 401 errors in GoCardless service.

- Add _make_authenticated_request helper that automatically handles 401 errors
- Clear token cache and retry once when encountering expired tokens
- Refactor all API methods to use centralized request handling
- Fix banks API to properly handle institutions response structure
- Eliminates need for container restarts when tokens expire

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Elisiário Couto
2025-09-24 14:58:45 +01:00
parent d211a14703
commit 36d698f7ce
2 changed files with 44 additions and 55 deletions

View File

@@ -32,7 +32,7 @@ async def get_bank_institutions(
countries=inst["countries"], countries=inst["countries"],
logo=inst.get("logo"), logo=inst.get("logo"),
) )
for inst in institutions_data for inst in institutions_data.get("results", [])
] ]
return APIResponse( return APIResponse(

View File

@@ -1,6 +1,6 @@
import json import json
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List from typing import Any, Dict
import httpx import httpx
from loguru import logger from loguru import logger
@@ -30,6 +30,27 @@ class GoCardlessService:
) )
self._token = None self._token = None
async def _make_authenticated_request(
self, method: str, url: str, **kwargs
) -> Dict[str, Any]:
"""Make authenticated request with automatic token refresh on 401"""
headers = await self._get_auth_headers()
async with httpx.AsyncClient() as client:
response = await client.request(method, url, headers=headers, **kwargs)
_log_rate_limits(response)
# If we get 401, clear token cache and retry once
if response.status_code == 401:
logger.warning("Got 401, clearing token cache and retrying")
self._token = None
headers = await self._get_auth_headers()
response = await client.request(method, url, headers=headers, **kwargs)
_log_rate_limits(response)
response.raise_for_status()
return response.json()
async def _get_auth_headers(self) -> Dict[str, str]: async def _get_auth_headers(self) -> Dict[str, str]:
"""Get authentication headers for GoCardless API""" """Get authentication headers for GoCardless API"""
token = await self._get_token() token = await self._get_token()
@@ -102,74 +123,42 @@ class GoCardlessService:
with open(auth_file, "w") as f: with open(auth_file, "w") as f:
json.dump(auth_data, f) json.dump(auth_data, f)
async def get_institutions(self, country: str = "PT") -> List[Dict[str, Any]]: async def get_institutions(self, country: str = "PT") -> Dict[str, Any]:
"""Get available bank institutions for a country""" """Get available bank institutions for a country"""
headers = await self._get_auth_headers() return await self._make_authenticated_request(
async with httpx.AsyncClient() as client: "GET", f"{self.base_url}/institutions/", params={"country": country}
response = await client.get(
f"{self.base_url}/institutions/",
headers=headers,
params={"country": country},
) )
_log_rate_limits(response)
response.raise_for_status()
return response.json()
async def create_requisition( async def create_requisition(
self, institution_id: str, redirect_url: str self, institution_id: str, redirect_url: str
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Create a bank connection requisition""" """Create a bank connection requisition"""
headers = await self._get_auth_headers() return await self._make_authenticated_request(
async with httpx.AsyncClient() as client: "POST",
response = await client.post(
f"{self.base_url}/requisitions/", f"{self.base_url}/requisitions/",
headers=headers,
json={"institution_id": institution_id, "redirect": redirect_url}, json={"institution_id": institution_id, "redirect": redirect_url},
) )
_log_rate_limits(response)
response.raise_for_status()
return response.json()
async def get_requisitions(self) -> Dict[str, Any]: async def get_requisitions(self) -> Dict[str, Any]:
"""Get all requisitions""" """Get all requisitions"""
headers = await self._get_auth_headers() return await self._make_authenticated_request(
async with httpx.AsyncClient() as client: "GET", f"{self.base_url}/requisitions/"
response = await client.get(
f"{self.base_url}/requisitions/", headers=headers
) )
_log_rate_limits(response)
response.raise_for_status()
return response.json()
async def get_account_details(self, account_id: str) -> Dict[str, Any]: async def get_account_details(self, account_id: str) -> Dict[str, Any]:
"""Get account details""" """Get account details"""
headers = await self._get_auth_headers() return await self._make_authenticated_request(
async with httpx.AsyncClient() as client: "GET", f"{self.base_url}/accounts/{account_id}/"
response = await client.get(
f"{self.base_url}/accounts/{account_id}/", headers=headers
) )
_log_rate_limits(response)
response.raise_for_status()
return response.json()
async def get_account_balances(self, account_id: str) -> Dict[str, Any]: async def get_account_balances(self, account_id: str) -> Dict[str, Any]:
"""Get account balances""" """Get account balances"""
headers = await self._get_auth_headers() return await self._make_authenticated_request(
async with httpx.AsyncClient() as client: "GET", f"{self.base_url}/accounts/{account_id}/balances/"
response = await client.get(
f"{self.base_url}/accounts/{account_id}/balances/", headers=headers
) )
_log_rate_limits(response)
response.raise_for_status()
return response.json()
async def get_account_transactions(self, account_id: str) -> Dict[str, Any]: async def get_account_transactions(self, account_id: str) -> Dict[str, Any]:
"""Get account transactions""" """Get account transactions"""
headers = await self._get_auth_headers() return await self._make_authenticated_request(
async with httpx.AsyncClient() as client: "GET", f"{self.base_url}/accounts/{account_id}/transactions/"
response = await client.get(
f"{self.base_url}/accounts/{account_id}/transactions/", headers=headers
) )
_log_rate_limits(response)
response.raise_for_status()
return response.json()