← Home

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    # bytes

Common exceptions

ExceptionWhen
requests.TimeoutServer didn’t respond within timeout
requests.ConnectionErrorDNS failure, refused connection
requests.HTTPErrorraise_for_status() on 4xx/5xx
requests.JSONDecodeErrorresponse 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 range

Tests

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

Resources