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.

Test Development Learning Exchange
Test Development Learning Exchange
Test Development Learning Exchange
How to Build a Self‑Healing API Test Client with Automatic Token Refresh

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 resp

Step 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.

Token refresh flow diagram
Token refresh flow diagram
PythonAPI testingrequeststoken refreshtenacity
Test Development Learning Exchange
Written by

Test Development Learning Exchange

Test Development Learning Exchange

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.