Introduction
When working with web APIs in Python, you will often encounter situations where your requests are denied due to authorization issues. This lab guides you through understanding and effectively handling unauthorized (401) responses when using the Python requests library. By learning proper error handling techniques, you will be able to build more resilient applications that gracefully manage authentication failures.
Understanding HTTP Authorization and Status Codes
Before we dive into handling unauthorized responses, it is important to understand what they are and why they occur.
HTTP Status Codes and the 401 Unauthorized Response
HTTP status codes are three-digit numbers that servers send in response to client requests. These codes are grouped into five categories:
- 1xx: Informational responses
- 2xx: Successful responses
- 3xx: Redirection messages
- 4xx: Client error responses
- 5xx: Server error responses
The 401 Unauthorized status code belongs to the 4xx category and indicates that the request lacks valid authentication credentials for the target resource. This is different from a 403 Forbidden response, which means the server understands the request but refuses to authorize it.
Setting Up Our Environment
Let us start by creating a directory for our project and installing the required packages.
- Open the terminal and create a new directory:
mkdir -p ~/project/python-auth-handling
cd ~/project/python-auth-handling
- Now, let us create a virtual environment and install the
requestspackage:
python -m venv venv
source venv/bin/activate
pip install requests
The output should look similar to:
Collecting requests
Downloading requests-2.28.2-py3-none-any.whl (62 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 62.8/62.8 KB 1.8 MB/s eta 0:00:00
[...additional output...]
Successfully installed certifi-2023.5.7 charset-normalizer-3.1.0 idna-3.4 requests-2.28.2 urllib3-1.26.16
Making a Simple Request
Now, let us create a Python script to make a request to a service that requires authentication. We will use the HTTPBin service, which provides endpoints for testing HTTP requests.
Create a new file named basic_request.py in the WebIDE:
import requests
def make_request():
url = "https://httpbin.org/basic-auth/user/pass"
response = requests.get(url)
print(f"Status Code: {response.status_code}")
if response.status_code == 200:
print("Request was successful.")
print(f"Response content: {response.text}")
elif response.status_code == 401:
print("Unauthorized: Authentication is required and has failed.")
else:
print(f"Received unexpected status code: {response.status_code}")
if __name__ == "__main__":
make_request()
Save the file and run it in the terminal:
python basic_request.py
You should see an output similar to:
Status Code: 401
Unauthorized: Authentication is required and has failed.
This is because we are trying to access an endpoint that requires basic authentication, but we have not provided any credentials.
Examining the Response
Let us modify our script to print more details about the response. Create a new file named examine_response.py:
import requests
def examine_response():
url = "https://httpbin.org/basic-auth/user/pass"
response = requests.get(url)
print(f"Status Code: {response.status_code}")
print(f"Headers: {response.headers}")
## The WWW-Authenticate header provides details about how to authenticate
if 'WWW-Authenticate' in response.headers:
print(f"WWW-Authenticate: {response.headers['WWW-Authenticate']}")
## Try to get JSON content, but it might not be JSON or might be empty
try:
print(f"Content: {response.json()}")
except requests.exceptions.JSONDecodeError:
print(f"Content (text): {response.text}")
if __name__ == "__main__":
examine_response()
Run this script:
python examine_response.py
The output will include the response headers and the WWW-Authenticate header, which tells the client how to authenticate:
Status Code: 401
Headers: {'date': '...', 'content-type': '...', 'content-length': '0', 'connection': 'close', 'server': 'gunicorn/19.9.0', 'www-authenticate': 'Basic realm="Fake Realm"', 'access-control-allow-origin': '*', 'access-control-allow-credentials': 'true'}
WWW-Authenticate: Basic realm="Fake Realm"
Content (text):
The WWW-Authenticate header with value Basic realm="Fake Realm" indicates that the server expects basic authentication.
Using Basic Authentication to Prevent 401 Errors
Now that we understand what causes a 401 Unauthorized response, let us learn how to prevent it by providing the correct authentication credentials.
Basic Authentication in Python Requests
The Python requests library makes it easy to add basic authentication to your requests. You can either:
- Pass the
authparameter with a tuple of username and password - Use the
HTTPBasicAuthclass fromrequests.auth
Let us modify our script to include basic authentication. Create a new file named basic_auth.py:
import requests
from requests.auth import HTTPBasicAuth
def make_authenticated_request():
url = "https://httpbin.org/basic-auth/user/pass"
## Method 1: Using the auth parameter with a tuple
response1 = requests.get(url, auth=("user", "pass"))
print("Method 1: Using auth tuple")
print(f"Status Code: {response1.status_code}")
if response1.status_code == 200:
print(f"Response content: {response1.json()}")
else:
print(f"Request failed with status code: {response1.status_code}")
print("\n" + "-"*50 + "\n")
## Method 2: Using the HTTPBasicAuth class
response2 = requests.get(url, auth=HTTPBasicAuth("user", "pass"))
print("Method 2: Using HTTPBasicAuth class")
print(f"Status Code: {response2.status_code}")
if response2.status_code == 200:
print(f"Response content: {response2.json()}")
else:
print(f"Request failed with status code: {response2.status_code}")
if __name__ == "__main__":
make_authenticated_request()
Run the script:
python basic_auth.py
You should see a successful response with status code 200:
Method 1: Using auth tuple
Status Code: 200
Response content: {'authenticated': True, 'user': 'user'}
--------------------------------------------------
Method 2: Using HTTPBasicAuth class
Status Code: 200
Response content: {'authenticated': True, 'user': 'user'}
Both methods achieve the same result - they add an Authorization header to the request with the base64-encoded username and password.
What Happens When Authentication Fails
Let us see what happens when we provide incorrect credentials. Create a new file named failed_auth.py:
import requests
def test_failed_authentication():
url = "https://httpbin.org/basic-auth/user/pass"
## Correct credentials
response_correct = requests.get(url, auth=("user", "pass"))
## Incorrect password
response_wrong_pass = requests.get(url, auth=("user", "wrong_password"))
## Incorrect username
response_wrong_user = requests.get(url, auth=("wrong_user", "pass"))
print("Correct credentials:")
print(f"Status Code: {response_correct.status_code}")
if response_correct.status_code == 200:
print(f"Response content: {response_correct.json()}")
print("\nIncorrect password:")
print(f"Status Code: {response_wrong_pass.status_code}")
print("\nIncorrect username:")
print(f"Status Code: {response_wrong_user.status_code}")
if __name__ == "__main__":
test_failed_authentication()
Run the script:
python failed_auth.py
You should see output similar to:
Correct credentials:
Status Code: 200
Response content: {'authenticated': True, 'user': 'user'}
Incorrect password:
Status Code: 401
Incorrect username:
Status Code: 401
Both incorrect username and incorrect password result in a 401 Unauthorized response.
Handling Authentication Errors
Now, let us implement error handling for authentication failures. Create a new file named handle_auth_errors.py:
import requests
from requests.exceptions import HTTPError
def make_authenticated_request(username, password):
url = "https://httpbin.org/basic-auth/user/pass"
try:
response = requests.get(url, auth=(username, password))
response.raise_for_status() ## Raises an HTTPError for bad responses (4xx or 5xx)
print(f"Authentication successful!")
print(f"Response content: {response.json()}")
return response
except HTTPError as e:
if response.status_code == 401:
print(f"Authentication failed: Invalid credentials")
## You might want to retry with different credentials here
return handle_authentication_failure()
else:
print(f"HTTP Error occurred: {e}")
return None
except Exception as e:
print(f"An error occurred: {e}")
return None
def handle_authentication_failure():
## In a real application, you might prompt the user for new credentials
## or use a token refresh mechanism
print("Attempting to authenticate with default credentials...")
return make_authenticated_request("user", "pass")
if __name__ == "__main__":
## First, try with incorrect credentials
print("Trying with incorrect credentials:")
make_authenticated_request("wrong_user", "wrong_pass")
print("\n" + "-"*50 + "\n")
## Then, try with correct credentials
print("Trying with correct credentials:")
make_authenticated_request("user", "pass")
Run the script:
python handle_auth_errors.py
The output should show how the error is handled:
Trying with incorrect credentials:
Authentication failed: Invalid credentials
Attempting to authenticate with default credentials...
Authentication successful!
Response content: {'authenticated': True, 'user': 'user'}
--------------------------------------------------
Trying with correct credentials:
Authentication successful!
Response content: {'authenticated': True, 'user': 'user'}
This script demonstrates a simple error handling pattern where we:
- Attempt to make the request with the provided credentials
- Use
raise_for_status()to raise an exception for 4xx/5xx responses - Handle the 401 error specifically, with a retry mechanism
- Handle other types of errors appropriately
Advanced Authentication Handling Strategies
In real-world applications, you often need more advanced strategies to handle authentication. Let us explore some common techniques.
Token-Based Authentication
Many modern APIs use token-based authentication instead of basic authentication. OAuth 2.0 is a common protocol that uses tokens for authentication and authorization.
Let us create a script that simulates token-based authentication. Create a file named token_auth.py:
import requests
import time
import json
## Simulated token storage - in a real app, this might be a database or secure storage
TOKEN_FILE = "token.json"
def get_stored_token():
"""Retrieve the stored token if it exists."""
try:
with open(TOKEN_FILE, "r") as f:
token_data = json.load(f)
## Check if token is expired
if token_data.get("expires_at", 0) > time.time():
return token_data["access_token"]
except (FileNotFoundError, json.JSONDecodeError):
pass
return None
def save_token(token, expires_in=3600):
"""Save the token with expiration time."""
token_data = {
"access_token": token,
"expires_at": time.time() + expires_in
}
with open(TOKEN_FILE, "w") as f:
json.dump(token_data, f)
def get_new_token():
"""Simulate obtaining a new token from an authentication service."""
## In a real application, this would make a request to the auth server
print("Obtaining new access token...")
## Simulating a token generation
new_token = f"simulated_token_{int(time.time())}"
save_token(new_token)
return new_token
def make_authenticated_request(url):
"""Make a request with token authentication, refreshing if needed."""
## Try to get the stored token
token = get_stored_token()
## If no valid token exists, get a new one
if not token:
token = get_new_token()
## Make the authenticated request
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(url, headers=headers)
## If unauthorized, the token might be invalid - get a new one and retry
if response.status_code == 401:
print("Token rejected. Getting a new token and retrying...")
token = get_new_token()
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(url, headers=headers)
return response
## For testing, we'll use httpbin's bearer auth endpoint
if __name__ == "__main__":
## Using httpbin.org/bearer which checks for the Authorization header
url = "https://httpbin.org/bearer"
## First, delete any existing token to simulate a fresh start
try:
import os
os.remove(TOKEN_FILE)
except FileNotFoundError:
pass
print("First request (should obtain a new token):")
response = make_authenticated_request(url)
print(f"Status code: {response.status_code}")
print(f"Response: {response.json()}")
print("\nSecond request (should use the stored token):")
response = make_authenticated_request(url)
print(f"Status code: {response.status_code}")
print(f"Response: {response.json()}")
Run the script:
python token_auth.py
The output should be similar to:
First request (should obtain a new token):
Obtaining new access token...
Status code: 200
Response: {'authenticated': True, 'token': 'simulated_token_1623456789'}
Second request (should use the stored token):
Status code: 200
Response: {'authenticated': True, 'token': 'simulated_token_1623456789'}
This script demonstrates token-based authentication with automatic token refresh when a token is rejected.
Implementing Retry Logic
Sometimes, authentication failures can be temporary. Let us implement a retry mechanism with exponential backoff. Create a file named retry_auth.py:
import requests
import time
import random
def make_request_with_retry(url, auth, max_retries=3, backoff_factor=0.5):
"""
Makes a request with retry logic and exponential backoff
Args:
url: URL to request
auth: Authentication tuple (username, password)
max_retries: Maximum number of retry attempts
backoff_factor: Factor by which to increase the delay between retries
"""
retries = 0
while retries <= max_retries:
try:
response = requests.get(url, auth=auth, timeout=10)
## If successful, return the response
if response.status_code == 200:
return response
## If unauthorized, we might want to handle differently
if response.status_code == 401:
print(f"Attempt {retries+1}/{max_retries+1}: Authentication failed")
## In a real app, we might refresh tokens here or prompt for new credentials
else:
print(f"Attempt {retries+1}/{max_retries+1}: Failed with status code {response.status_code}")
## If we've reached max retries, give up
if retries == max_retries:
print("Maximum retry attempts reached.")
return response
## Calculate delay with exponential backoff and jitter
delay = backoff_factor * (2 ** retries) + random.uniform(0, 0.1)
print(f"Retrying in {delay:.2f} seconds...")
time.sleep(delay)
retries += 1
except requests.exceptions.RequestException as e:
print(f"Request exception: {e}")
## If we've reached max retries, give up
if retries == max_retries:
print("Maximum retry attempts reached.")
raise
## Calculate delay with exponential backoff and jitter
delay = backoff_factor * (2 ** retries) + random.uniform(0, 0.1)
print(f"Retrying in {delay:.2f} seconds...")
time.sleep(delay)
retries += 1
if __name__ == "__main__":
url = "https://httpbin.org/basic-auth/user/pass"
print("Testing with incorrect credentials:")
response = make_request_with_retry(url, auth=("wrong_user", "wrong_pass"))
print(f"Final status code: {response.status_code}")
print("\nTesting with correct credentials:")
response = make_request_with_retry(url, auth=("user", "pass"))
print(f"Final status code: {response.status_code}")
print(f"Response content: {response.json()}")
Run the script:
python retry_auth.py
The output should be similar to:
Testing with incorrect credentials:
Attempt 1/4: Authentication failed
Retrying in 0.54 seconds...
Attempt 2/4: Authentication failed
Retrying in 1.05 seconds...
Attempt 3/4: Authentication failed
Retrying in 2.08 seconds...
Attempt 4/4: Authentication failed
Maximum retry attempts reached.
Final status code: 401
Testing with correct credentials:
Final status code: 200
Response content: {'authenticated': True, 'user': 'user'}
This script demonstrates a retry mechanism with exponential backoff, which is a common pattern when dealing with network requests that might fail temporarily.
Creating a Reusable Authentication Session
For multiple requests to the same service, it is often more efficient to create a session that will maintain the authentication information. Create a file named auth_session.py:
import requests
def create_authenticated_session(base_url, username, password):
"""
Creates and returns a requests session with basic authentication
Args:
base_url: Base URL for the API
username: Username for authentication
password: Password for authentication
Returns:
A configured requests.Session object
"""
session = requests.Session()
session.auth = (username, password)
session.headers.update({
'User-Agent': 'MyApp/1.0',
'Accept': 'application/json'
})
## Test the authentication
test_url = f"{base_url}/basic-auth/{username}/{password}"
try:
response = session.get(test_url)
response.raise_for_status()
print(f"Authentication successful for user: {username}")
except requests.exceptions.HTTPError as e:
print(f"Authentication failed: {e}")
## In a real app, you might raise an exception here or handle it differently
return session
def make_authenticated_requests(session, base_url):
"""
Makes multiple requests using the authenticated session
Args:
session: The authenticated requests.Session
base_url: Base URL for the API
"""
## Make a request to a different endpoint
response1 = session.get(f"{base_url}/get")
print(f"\nRequest 1 status code: {response1.status_code}")
print(f"Request 1 response: {response1.json()}")
## Make another request
response2 = session.get(f"{base_url}/headers")
print(f"\nRequest 2 status code: {response2.status_code}")
print(f"Request 2 response: {response2.json()}")
if __name__ == "__main__":
base_url = "https://httpbin.org"
## Create an authenticated session
session = create_authenticated_session(base_url, "user", "pass")
## Use the session for multiple requests
make_authenticated_requests(session, base_url)
Run the script:
python auth_session.py
The output should be similar to:
Authentication successful for user: user
Request 1 status code: 200
Request 1 response: {'args': {}, 'headers': {...}, 'origin': '...', 'url': 'https://httpbin.org/get'}
Request 2 status code: 200
Request 2 response: {'headers': {...}}
Using a session has several advantages:
- It reuses the underlying TCP connection, making subsequent requests faster
- It automatically includes authentication credentials with each request
- It maintains cookies across requests, which is useful for session-based authentication
Handling Real-World Authentication Challenges
In real-world applications, you often encounter more complex authentication scenarios. Let us explore some practical examples and best practices.
Implementing a Custom Authentication Class
The requests library allows you to create custom authentication handlers by subclassing the requests.auth.AuthBase class. This is useful when working with APIs that have custom authentication schemes.
Create a file named custom_auth.py:
import requests
from requests.auth import AuthBase
import time
import hashlib
class CustomAuth(AuthBase):
"""
Example of a custom authentication mechanism that adds a timestamp and HMAC signature
This is similar to how many APIs authenticate requests (like AWS, Stripe, etc.)
"""
def __init__(self, api_key, api_secret):
self.api_key = api_key
self.api_secret = api_secret
def __call__(self, request):
## Add timestamp to the request
timestamp = str(int(time.time()))
## Add the API key and timestamp as headers
request.headers['X-API-Key'] = self.api_key
request.headers['X-Timestamp'] = timestamp
## Create a signature based on the request
## In a real implementation, this would include more request elements
method = request.method
path = request.path_url
## Create a simple signature (in real apps, use proper HMAC)
signature_data = f"{method}{path}{timestamp}{self.api_secret}".encode('utf-8')
signature = hashlib.sha256(signature_data).hexdigest()
## Add the signature to the headers
request.headers['X-Signature'] = signature
return request
def test_custom_auth():
"""Test our custom authentication with httpbin.org"""
url = "https://httpbin.org/headers"
## Create our custom auth handler
auth = CustomAuth(api_key="test_key", api_secret="test_secret")
## Make the request with our custom auth
response = requests.get(url, auth=auth)
print(f"Status Code: {response.status_code}")
print(f"Response Headers: {response.json()}")
## Verify our custom headers were sent
headers = response.json()['headers']
for header in ['X-Api-Key', 'X-Timestamp', 'X-Signature']:
if header.upper() in headers:
print(f"{header} was sent: {headers[header.upper()]}")
else:
print(f"{header} was not found in the response")
if __name__ == "__main__":
test_custom_auth()
Run the script:
python custom_auth.py
The output should include the custom headers in the response:
Status Code: 200
Response Headers: {'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.28.2', 'X-Api-Key': 'test_key', 'X-Signature': '...', 'X-Timestamp': '1623456789'}}
X-Api-Key was sent: test_key
X-Timestamp was sent: 1623456789
X-Signature was sent: ...
This example shows how to implement a custom authentication scheme that includes a timestamp and signature, similar to what many commercial APIs use.
Handling Rate Limiting and 429 Responses
Many APIs implement rate limiting to prevent abuse. When you exceed the rate limit, the server typically responds with a 429 Too Many Requests status code. Let us create a script to handle this scenario.
Create a file named rate_limit_handler.py:
import requests
import time
def make_request_with_rate_limit_handling(url, max_retries=5):
"""
Makes a request with handling for rate limits (429 responses)
Args:
url: URL to request
max_retries: Maximum number of retry attempts
"""
retries = 0
while retries <= max_retries:
response = requests.get(url)
## If not rate limited, return the response
if response.status_code != 429:
return response
## We got rate limited, check if we have retry information
retry_after = response.headers.get('Retry-After')
## If we have retry information, wait that long
if retry_after:
wait_time = int(retry_after)
print(f"Rate limited. Waiting for {wait_time} seconds as specified by Retry-After header.")
time.sleep(wait_time)
else:
## If no retry information, use exponential backoff
wait_time = 2 ** retries
print(f"Rate limited. No Retry-After header. Using exponential backoff: {wait_time} seconds.")
time.sleep(wait_time)
retries += 1
if retries > max_retries:
print(f"Maximum retries ({max_retries}) exceeded.")
return response
## For demonstration, we'll simulate rate limiting using httpbin's status endpoint
if __name__ == "__main__":
## First request should succeed
print("Making request to endpoint that returns 200:")
response = make_request_with_rate_limit_handling("https://httpbin.org/status/200")
print(f"Final status code: {response.status_code}")
## Simulate being rate limited initially, then succeeding
print("\nSimulating rate limiting scenario:")
## We'll make 3 requests in sequence to different status codes (429, 429, 200)
## to simulate being rate limited twice and then succeeding
urls = [
"https://httpbin.org/status/429", ## First will be rate limited
"https://httpbin.org/status/429", ## Second will also be rate limited
"https://httpbin.org/status/200" ## Third will succeed
]
for i, url in enumerate(urls):
print(f"\nRequest {i+1}:")
response = make_request_with_rate_limit_handling(url, max_retries=1)
print(f"Final status code: {response.status_code}")
Run the script:
python rate_limit_handler.py
The output will simulate handling rate limiting:
Making request to endpoint that returns 200:
Final status code: 200
Simulating rate limiting scenario:
Request 1:
Rate limited. No Retry-After header. Using exponential backoff: 1 seconds.
Rate limited. No Retry-After header. Using exponential backoff: 2 seconds.
Maximum retries (1) exceeded.
Final status code: 429
Request 2:
Rate limited. No Retry-After header. Using exponential backoff: 1 seconds.
Rate limited. No Retry-After header. Using exponential backoff: 2 seconds.
Maximum retries (1) exceeded.
Final status code: 429
Request 3:
Final status code: 200
This demonstrates how to handle rate limiting by respecting the Retry-After header or implementing exponential backoff.
Complete Error Handling Solution
Finally, let us put everything together into a comprehensive error handling solution. Create a file named complete_auth_handler.py:
import requests
import time
import random
import json
from requests.exceptions import RequestException, HTTPError, ConnectionError, Timeout
class AuthenticationManager:
"""Manages authentication tokens and credentials"""
def __init__(self, token_file="auth_token.json"):
self.token_file = token_file
self.token = self.load_token()
def load_token(self):
"""Load token from file storage"""
try:
with open(self.token_file, "r") as f:
token_data = json.load(f)
## Check if token is expired
if token_data.get("expires_at", 0) > time.time():
return token_data.get("access_token")
except (FileNotFoundError, json.JSONDecodeError):
pass
return None
def save_token(self, token, expires_in=3600):
"""Save token to file storage"""
token_data = {
"access_token": token,
"expires_at": time.time() + expires_in
}
with open(self.token_file, "w") as f:
json.dump(token_data, f)
def get_token(self):
"""Get a valid token, refreshing if necessary"""
if not self.token:
self.token = self.refresh_token()
return self.token
def refresh_token(self):
"""Refresh the authentication token"""
## In a real app, this would make a call to the auth server
print("Getting a new authentication token...")
new_token = f"refreshed_token_{int(time.time())}"
self.save_token(new_token)
return new_token
def invalidate_token(self):
"""Invalidate the current token"""
self.token = None
try:
with open(self.token_file, "w") as f:
json.dump({}, f)
except Exception:
pass
class APIClient:
"""Client for making API requests with comprehensive error handling"""
def __init__(self, base_url, auth_manager=None):
self.base_url = base_url
self.auth_manager = auth_manager or AuthenticationManager()
self.session = requests.Session()
def make_request(self, method, endpoint, **kwargs):
"""
Make an API request with comprehensive error handling
Args:
method: HTTP method (get, post, etc.)
endpoint: API endpoint to call
**kwargs: Additional arguments to pass to requests
Returns:
Response object or None if all retries failed
"""
url = f"{self.base_url}{endpoint}"
max_retries = kwargs.pop("max_retries", 3)
retry_backoff_factor = kwargs.pop("retry_backoff_factor", 0.5)
## Add authentication if we have an auth manager
if self.auth_manager:
token = self.auth_manager.get_token()
headers = kwargs.get("headers", {})
headers["Authorization"] = f"Bearer {token}"
kwargs["headers"] = headers
## Make the request with retries
for retry in range(max_retries + 1):
try:
request_func = getattr(self.session, method.lower())
response = request_func(url, **kwargs)
## Handle different status codes
if response.status_code == 200:
return response
elif response.status_code == 401:
print("Unauthorized: Token may be invalid")
if self.auth_manager and retry < max_retries:
print("Refreshing authentication token...")
self.auth_manager.invalidate_token()
token = self.auth_manager.get_token()
kwargs.get("headers", {})["Authorization"] = f"Bearer {token}"
continue
elif response.status_code == 429:
if retry < max_retries:
retry_after = response.headers.get("Retry-After")
if retry_after:
wait_time = int(retry_after)
else:
wait_time = retry_backoff_factor * (2 ** retry) + random.uniform(0, 0.1)
print(f"Rate limited. Waiting {wait_time:.2f} seconds before retry {retry + 1}/{max_retries}")
time.sleep(wait_time)
continue
else:
print("Rate limit retries exhausted")
elif 500 <= response.status_code < 600:
if retry < max_retries:
wait_time = retry_backoff_factor * (2 ** retry) + random.uniform(0, 0.1)
print(f"Server error ({response.status_code}). Retrying in {wait_time:.2f} seconds...")
time.sleep(wait_time)
continue
else:
print(f"Server error retries exhausted")
## If we get here, we're returning the response as-is
return response
except ConnectionError:
if retry < max_retries:
wait_time = retry_backoff_factor * (2 ** retry) + random.uniform(0, 0.1)
print(f"Connection error. Retrying in {wait_time:.2f} seconds...")
time.sleep(wait_time)
else:
print("Connection error retries exhausted")
raise
except Timeout:
if retry < max_retries:
wait_time = retry_backoff_factor * (2 ** retry) + random.uniform(0, 0.1)
print(f"Request timed out. Retrying in {wait_time:.2f} seconds...")
time.sleep(wait_time)
else:
print("Timeout retries exhausted")
raise
except RequestException as e:
print(f"Request failed: {e}")
raise
return None
def get(self, endpoint, **kwargs):
"""Make a GET request"""
return self.make_request("get", endpoint, **kwargs)
def post(self, endpoint, **kwargs):
"""Make a POST request"""
return self.make_request("post", endpoint, **kwargs)
## Additional methods can be added for other HTTP verbs
## Test the client with httpbin
if __name__ == "__main__":
## Clean any existing token files
import os
try:
os.remove("auth_token.json")
except FileNotFoundError:
pass
client = APIClient("https://httpbin.org")
print("Making a GET request to /get:")
response = client.get("/get")
print(f"Status code: {response.status_code}")
print(f"Response content: {response.json()}")
print("\nSimulating an unauthorized response:")
## For demonstration purposes, we'll use httpbin's status endpoint to simulate a 401
response = client.get("/status/401")
print(f"Status code: {response.status_code}")
print("\nSimulating a rate-limited response:")
## Simulate a 429 response
response = client.get("/status/429")
print(f"Status code: {response.status_code}")
Run the script:
python complete_auth_handler.py
The output should demonstrate the comprehensive error handling:
Getting a new authentication token...
Making a GET request to /get:
Status code: 200
Response content: {'args': {}, 'headers': {...}, 'origin': '...', 'url': 'https://httpbin.org/get'}
Simulating an unauthorized response:
Unauthorized: Token may be invalid
Refreshing authentication token...
Getting a new authentication token...
Unauthorized: Token may be invalid
Refreshing authentication token...
Getting a new authentication token...
Unauthorized: Token may be invalid
Refreshing authentication token...
Getting a new authentication token...
Unauthorized: Token may be invalid
Status code: 401
Simulating a rate-limited response:
Rate limited. Waiting 0.54 seconds before retry 1/3
Rate limited. Waiting 1.04 seconds before retry 2/3
Rate limited. Waiting 2.07 seconds before retry 3/3
Rate limit retries exhausted
Status code: 429
This comprehensive example demonstrates many best practices for handling authentication and other API-related errors in a production-ready Python application.
Summary
In this lab, you have learned how to effectively handle unauthorized responses in Python requests. We covered several important aspects:
Understanding HTTP Authentication and Status Codes: You learned what 401 Unauthorized responses mean and how to identify them in your Python applications.
Basic Authentication: You implemented basic authentication using both the
authparameter and theHTTPBasicAuthclass, and learned how to handle authentication failures.Advanced Authentication Strategies: You explored token-based authentication, retry logic with exponential backoff, and creating reusable authenticated sessions.
Real-World Authentication Challenges: You implemented a custom authentication class, handled rate limiting, and created a comprehensive error handling solution for production-ready applications.
These techniques will help you build more robust and resilient Python applications that can gracefully handle authentication failures and other related errors when interacting with web APIs.
For further learning, consider exploring:
- OAuth 2.0 authentication flows
- JWT (JSON Web Tokens) for stateless authentication
- API key rotation strategies
- Secure credential storage



