122. Requests
HTTP API calls with the requests library
122. Requests
🌐 Aryan is calling the LLM API directly with requests.post(). He’s forgotten to set the Content-Type header, not checking for HTTP errors before reading the response body, and missing a timeout.
💡 Fun fact: The requests library was created by Kenneth Reitz in 2011 with the explicit goal of making HTTP “for humans.” It became so popular that it’s now one of the most downloaded Python packages ever, with over 300 million monthly downloads on PyPI. The requests library doesn’t ship with Python’s standard library — but Python’s built-in urllib does. requests wraps urllib with a far simpler API, so urllib.request.urlopen(req) with all its boilerplate gets replaced by just requests.get(url).
⚠️ Watch out: Forgetting response.raise_for_status() is one of the most common bugs in HTTP code. When an API returns a 429 Too Many Requests or 500 Internal Server Error, requests does NOT raise an exception by default — .json() still works and returns the error body dict. Your code then crashes much later with a confusing KeyError when it tries to read data['content'][0]['text']. Always call raise_for_status() immediately after .post(), before touching the body.
🤔 Think about it: HTTP success is the range 200–299, not just 200. 201 Created is returned when you create a new resource, 202 Accepted for async operations. If is_success() only checks == 200, it incorrectly treats 201 as failure. What other assumptions might your code make about HTTP that are subtly wrong? For example: can a GET request have a body? Can a POST return a redirect?
Learning objectives
- Send POST requests with JSON body using requests.post()
- Set required headers including Content-Type and auth headers
- Always include a timeout to prevent infinite hangs
- Use raise_for_status() to detect HTTP errors before parsing body
- Check the full 2xx range for success, not just status 200
Key concepts
- requests.post(url, json=, headers=, timeout=)
- response.raise_for_status() — HTTPError on 4xx/5xx
- response.json() — parse JSON body
- requests.Timeout / ConnectionError / HTTPError
- 2xx success range (200–299)
Try it
Concept detail
HTTP Calls with requests
The requests library is Python’s standard for HTTP. Install: pip install requests.
POST with JSON body
response = requests.post(
url,
headers={'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'},
json=payload, # auto-serializes dict, sets Content-Type
timeout=30, # seconds before TimeoutError
)Always check the status
# Option 1: raise immediately on error
response.raise_for_status() # raises requests.HTTPError on 4xx/5xx
# Option 2: check manually
if not (200 <= response.status_code < 300):
raise ValueError(f"API error: {response.status_code}")Read the body
data = response.json() # parse JSON response body
text = response.text # raw string body
raw = response.content # bytesCommon exceptions
| Exception | When |
|---|---|
requests.Timeout | Server didn’t respond within timeout |
requests.ConnectionError | DNS failure, refused connection |
requests.HTTPError | raise_for_status() on 4xx/5xx |
requests.JSONDecodeError | response body is not valid JSON |
Solution
import json
from unittest.mock import MagicMock
# Simulate requests library for Pyodide (no real network calls)
class _FakeRequests:
class HTTPError(Exception):
def __init__(self, response=None):
self.response = response
class Response:
def __init__(self, status_code=200, json_body=None):
self.status_code = status_code
self._json_body = json_body or {}
def json(self):
return self._json_body
def raise_for_status(self):
if self.status_code >= 400:
raise _FakeRequests.HTTPError(response=self)
@staticmethod
def post(url, headers=None, json=None, timeout=None):
raise RuntimeError("Real network call — replace with mock in tests")
requests = _FakeRequests()
BASE_URL = 'https://api.anthropic.com/v1/messages'
def call_llm(prompt: str, api_key: str) -> str:
"""Send a prompt to the LLM API and return the response text."""
payload = {
'model': 'claude-3-haiku-20240307',
'max_tokens': 256,
'messages': [{'role': 'user', 'content': prompt}],
}
response = requests.post(
BASE_URL,
headers={
'x-api-key': api_key,
'anthropic-version': '2023-06-01',
'content-type': 'application/json', # explicit Content-Type
},
json=payload,
timeout=30, # seconds — prevents infinite hangs
)
response.raise_for_status() # raises HTTPError for 4xx / 5xx
data = response.json()
return data['content'][0]['text']
def is_success(response) -> bool:
"""Return True if the HTTP response indicates success."""
return 200 <= response.status_code < 300 # full 2xx rangeTests
from unittest.mock import MagicMock
def _mock_response(status=200, json_body=None, raise_http_error=False):
r = MagicMock()
r.status_code = status
r.json.return_value = json_body or {'content': [{'text': 'Test response'}]}
if raise_http_error:
r.raise_for_status.side_effect = requests.HTTPError(response=r)
else:
r.raise_for_status.return_value = None
return r
def test_call_llm_returns_text():
mock_resp = _mock_response(200, {'content': [{'text': 'hello'}]})
original_post = requests.post
requests.post = MagicMock(return_value=mock_resp)
try:
result = call_llm('test prompt', 'fake-key')
finally:
requests.post = original_post
assert result == 'hello'
def test_call_llm_sends_content_type():
mock_resp = _mock_response()
original_post = requests.post
mock_post = MagicMock(return_value=mock_resp)
requests.post = mock_post
try:
call_llm('prompt', 'key')
finally:
requests.post = original_post
_, kwargs = mock_post.call_args
headers = kwargs.get('headers', {})
assert 'content-type' in headers or 'Content-Type' in headers, (
'Missing Content-Type header'
)
def test_call_llm_sends_timeout():
mock_resp = _mock_response()
original_post = requests.post
mock_post = MagicMock(return_value=mock_resp)
requests.post = mock_post
try:
call_llm('prompt', 'key')
finally:
requests.post = original_post
_, kwargs = mock_post.call_args
assert 'timeout' in kwargs, 'Missing timeout parameter'
assert kwargs['timeout'] > 0
def test_call_llm_raises_on_http_error():
mock_resp = _mock_response(status=429, raise_http_error=True)
original_post = requests.post
requests.post = MagicMock(return_value=mock_resp)
try:
raised = False
try:
call_llm('prompt', 'key')
except requests.HTTPError:
raised = True
assert raised, 'Should have raised HTTPError on 429'
finally:
requests.post = original_post
def test_is_success_200():
r = MagicMock(); r.status_code = 200
assert is_success(r) is True
def test_is_success_201():
r = MagicMock(); r.status_code = 201
assert is_success(r) is True, '201 Created is also a success status'
def test_is_success_404():
r = MagicMock(); r.status_code = 404
assert is_success(r) is False
def test_is_success_500():
r = MagicMock(); r.status_code = 500
assert is_success(r) is False