How to Build a Self‑Healing API Test Client with Automatic Token Refresh
Learn how to implement an automatic token‑refresh mechanism for API automation tests using Python’s requests and tenacity libraries, covering token refresh strategies, intercepting 401 responses, retry logic, client design, and production‑grade enhancements to ensure robust, unattended test execution.
1. Token expiration problem in API automation
When multiple test cases share a token, the token may expire (e.g., after 2 hours) and cause unexpected 401 Unauthorized responses. Manual token updates are impractical for CI/CD pipelines, so an automatic refresh mechanism is required.
2. Common token‑refresh patterns
Mode 1 – Refresh token (recommended)
After a successful login the server returns an access_token (short‑lived), a refresh_token (long‑lived) and an expires_in field indicating the access token’s lifetime.
{
"access_token": "eyJhbGci...",
"refresh_token": "def502...",
"expires_in": 7200
}When the access_token expires, the client can exchange the refresh_token for a new access token without re‑logging in.
Mode 2 – Re‑login (fallback)
If no refresh token is available or it has also expired, the client must call the login endpoint again to obtain fresh credentials. The automation should prioritize the refresh‑token flow and fall back to re‑login only when necessary.
3. Refresh workflow
Send the original request.
If the response status is 401:
Call the refresh endpoint (or re‑login) to obtain a new token.
Retry the original request with the new token.
Return the final response.
4. Python implementation (requests + tenacity)
Step 1 – Client with automatic refresh
# token_client.py
import time, logging, requests
from tenacity import retry, stop_after_attempt, retry_if_result
class TokenClient:
def __init__(self, base_url, username, password):
self.base_url = base_url
self.username = username
self.password = password
self.access_token = None
self.refresh_token = None
self.expires_at = None
self._login()
def _login(self):
"""Obtain tokens via login endpoint"""
resp = requests.post(f"{self.base_url}/auth/login", json={"username": self.username, "password": self.password})
resp.raise_for_status()
data = resp.json()
self.access_token = data["access_token"]
self.refresh_token = data.get("refresh_token")
self.expires_at = time.time() + data.get("expires_in", 0)
def _refresh(self):
"""Refresh the access token using the refresh token"""
if not self.refresh_token:
self._login()
return
try:
resp = requests.post(f"{self.base_url}/auth/refresh", json={"refresh_token": self.refresh_token})
resp.raise_for_status()
data = resp.json()
self.access_token = data["access_token"]
self.refresh_token = data.get("refresh_token", self.refresh_token)
self.expires_at = time.time() + data.get("expires_in", 0)
logging.info("Access token refreshed successfully")
except requests.HTTPError:
logging.warning("Refresh failed, falling back to login")
self._login()
def _is_unauthorized(self, response):
return response.status_code == 401
@retry(stop=stop_after_attempt(2), retry=retry_if_result(_is_unauthorized), reraise=True)
def request(self, method, url, **kwargs):
# Proactively refresh if token is about to expire (e.g., within 60 s)
if self.expires_at and self.expires_at - time.time() < 60:
self._refresh()
headers = kwargs.setdefault("headers", {})
headers["Authorization"] = f"Bearer {self.access_token}"
resp = requests.request(method, url, **kwargs)
if self._is_unauthorized(resp):
# Trigger retry which will call _refresh on the next attempt
self._refresh()
return respStep 2 – Usage in test cases
# test_profile.py
import pytest
from token_client import TokenClient
@pytest.fixture(scope="session")
def client():
return TokenClient(
base_url="https://api.test.com",
username="test_user",
password="secure_password"
)
def test_get_profile(client):
resp = client.request("GET", "https://api.test.com/profile")
assert resp.status_code == 200
assert "email" in resp.json()5. Production‑grade enhancements
Record token expiration time ( self.expires_at = time.time() + data["expires_in"]) and refresh proactively before it expires.
Support mixed authentication (e.g., Cookie + Token) by extending the header‑setting logic.
Log refresh actions for observability ( logging.info("Access token expired, refreshing…")).
Externalize configuration (username, password, URLs) via environment variables or a .env file to avoid hard‑coding credentials.
6. Scenarios requiring manual intervention
User account disabled or password changed.
Refresh token revoked (e.g., remote login).
401 response caused by reasons other than token expiration (e.g., insufficient permissions).
In these cases the automation should stop retrying and raise a clear exception for rapid troubleshooting.
Conclusion
Automatic token refresh is essential for reliable, unattended API test automation. Implementing a closed loop of “intercept 401 → smart refresh → retry” makes test scripts robust against token expiry and suitable for complex production environments.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
