Skip to content

HttpPlugin Guide

HttpPlugin intercepts HTTP calls made through httpx (sync and async), requests, urllib, and aiohttp (if installed). It requires the bigfoot[http] extra. For aiohttp support, also install bigfoot[aiohttp].

Installation

pip install bigfoot[http]              # httpx, requests, urllib
pip install bigfoot[aiohttp]           # + aiohttp support
pip install bigfoot[http,aiohttp]      # both

bigfoot[http] installs httpx>=0.25.0 and requests>=2.31.0. bigfoot[aiohttp] installs aiohttp>=3.9.0.

Setup

In pytest, access HttpPlugin through the bigfoot.http proxy. It auto-creates the plugin for the current test on first use — no explicit instantiation needed:

import bigfoot

def test_api():
    bigfoot.http.mock_response("GET", "https://api.example.com/users", json={"users": []})

    with bigfoot:
        import httpx
        response = httpx.get("https://api.example.com/users")

    bigfoot.http.assert_request("GET", "https://api.example.com/users",
                                headers=IsMapping(), body=None)

For manual use outside pytest, construct HttpPlugin explicitly:

from bigfoot import StrictVerifier
from bigfoot.plugins.http import HttpPlugin

verifier = StrictVerifier()
http = HttpPlugin(verifier)

Each verifier may have at most one HttpPlugin. A second HttpPlugin(verifier) raises ValueError.

Registering mock responses

Use bigfoot.http.mock_response(method, url, ...) to register a response before entering the sandbox:

bigfoot.http.mock_response("GET", "https://api.example.com/users", json={"users": []})

Parameters:

Parameter Type Default Description
method str required HTTP method, case-insensitive ("GET", "POST", etc.)
url str required Full URL to match, including scheme and host
json object None Response body serialized as JSON; sets content-type: application/json
body str \| bytes \| None None Raw response body; mutually exclusive with json
status int 200 HTTP status code
headers dict[str, str] \| None None Additional response headers
params dict[str, str] \| None None Query parameters that must be present in the request URL
required bool True Whether an unused mock causes UnusedMocksError at teardown

json and body are mutually exclusive; providing both raises ValueError.

FIFO ordering

Multiple mock_response() calls for the same method+URL are consumed in registration order. The first matching request gets the first registered response, and so on. If a request arrives with no matching mock remaining, UnmockedInteractionError is raised.

bigfoot.http.mock_response("GET", "https://api.example.com/token", json={"token": "first"})
bigfoot.http.mock_response("GET", "https://api.example.com/token", json={"token": "second"})

Optional responses

Mark a mock response as optional with required=False:

bigfoot.http.mock_response("GET", "https://api.example.com/health", json={"ok": True}, required=False)

An optional mock that is never triggered does not cause UnusedMocksError at teardown.

URL matching

bigfoot matches on scheme, host, path, and (if params is provided) query parameters. Query parameters in the actual URL that are not listed in params are ignored.

# Matches https://api.example.com/search?q=hello&page=2 if params={"q": "hello"}
bigfoot.http.mock_response("GET", "https://api.example.com/search", json={...}, params={"q": "hello"})

Asserting HTTP interactions

Use bigfoot.http.assert_request() to assert interactions after the sandbox exits:

import bigfoot, httpx

def test_users():
    bigfoot.http.mock_response("GET", "https://api.example.com/users", json=[])

    with bigfoot:
        response = httpx.get("https://api.example.com/users")

    bigfoot.http.assert_request("GET", "https://api.example.com/users",
                                headers=IsMapping(), body=None)

assert_request() requires all assertable request fields. Omitting any of method, url, headers, or body raises MissingAssertionFieldsError. Use IsMapping() from dirty-equals for headers when you want to assert type without exact matching, or ANY from unittest.mock.

assert_request() is a convenience wrapper around the lower-level verifier.assert_interaction() call:

# Convenience (recommended):
bigfoot.http.assert_request("GET", "https://api.example.com/users",
                            headers=IsMapping(), body=None)

# Equivalent low-level call:
bigfoot.assert_interaction(bigfoot.http.request, method="GET", url="https://api.example.com/users",
                           request_headers=IsMapping(), request_body=None)

Parameters for assert_request():

Parameter Description
method HTTP method, uppercase
url Full URL as received
headers Request headers dict
body Request body decoded as UTF-8

Using with httpx sync

import bigfoot, httpx

def test_httpx_sync():
    bigfoot.http.mock_response("GET", "https://api.example.com/data", json={"value": 42})

    with bigfoot:
        response = httpx.get("https://api.example.com/data")
        assert response.status_code == 200
        assert response.json() == {"value": 42}

    bigfoot.http.assert_request("GET", "https://api.example.com/data",
                                headers=IsMapping(), body=None)

Using with httpx async

import bigfoot, httpx

async def test_httpx_async():
    bigfoot.http.mock_response("POST", "https://api.example.com/items", json={"id": 1}, status=201)

    async with bigfoot:
        async with httpx.AsyncClient() as client:
            response = await client.post("https://api.example.com/items", json={"name": "widget"})
        assert response.status_code == 201

    bigfoot.http.assert_request("POST", "https://api.example.com/items",
                                headers=IsMapping(), body=None)

Using with requests

import bigfoot, requests

def test_requests():
    bigfoot.http.mock_response("DELETE", "https://api.example.com/items/99", status=204)

    with bigfoot:
        response = requests.delete("https://api.example.com/items/99")
        assert response.status_code == 204

    bigfoot.http.assert_request("DELETE", "https://api.example.com/items/99",
                                headers=IsMapping(), body=None)

UnmockedInteractionError for HTTP

When HTTP code fires a request with no matching mock, bigfoot raises UnmockedInteractionError with a hint:

Unexpected HTTP request: GET https://api.example.com/data

  To mock this request, add before your sandbox:
    bigfoot.http.mock_response("GET", "https://api.example.com/data", json={...})

  Or to mark it optional:
    bigfoot.http.mock_response("GET", "https://api.example.com/data", json={...}, required=False)

ConflictError

At sandbox entry, HttpPlugin checks whether httpx.HTTPTransport.handle_request, httpx.AsyncHTTPTransport.handle_async_request, and requests.adapters.HTTPAdapter.send have already been patched by another library. If any of these have been modified by a third party (respx, responses, httpretty, or an unknown library), bigfoot raises ConflictError:

ConflictError: target='httpx.HTTPTransport.handle_request', patcher='respx'

Nested bigfoot sandboxes use reference counting and do not conflict with each other.

Pass-Through: Real HTTP Calls

bigfoot.http.pass_through(method, url) registers a permanent routing rule. When an incoming request matches the rule and no mock response matches first, the real HTTP call is made through the original transport (bypassing bigfoot's interception layer). The interaction is still recorded on the timeline and must be asserted like any other interaction.

Pass-through rules are routing hints, not assertions. An unused pass-through rule does not raise UnusedMocksError at teardown.

import bigfoot, httpx

def test_mixed():
    bigfoot.http.mock_response("GET", "https://api.example.com/cached", json={"data": "cached"})
    bigfoot.http.pass_through("GET", "https://api.example.com/live")

    with bigfoot:
        mocked = httpx.get("https://api.example.com/cached")   # returns mock response
        real   = httpx.get("https://api.example.com/live")     # makes real HTTP call

    bigfoot.http.assert_request("GET", "https://api.example.com/cached",
                                headers=IsMapping(), body=None)
    bigfoot.http.assert_request("GET", "https://api.example.com/live",
                                headers=IsMapping(), body=None)

Mock responses are checked before pass-through rules. If a mock matches, the pass-through rule is not evaluated for that request. If no mock matches and a pass-through rule matches, the real call is made. If neither matches, UnmockedInteractionError is raised.

Requiring response assertions

By default, assert_request() asserts only the four request fields (method, url, request_headers, request_body) and returns None. The require_response feature changes this behavior so that assert_request() returns an HttpAssertionBuilder that must be completed with a chained .assert_response() call. This ensures all seven fields (four request + three response) are always asserted.

Enabling via configuration

Add to your pyproject.toml:

[tool.bigfoot.http]
require_response = true

With this setting, every assert_request() call returns an HttpAssertionBuilder:

import bigfoot, httpx

def test_api_with_response():
    bigfoot.http.mock_response("GET", "https://api.example.com/users", json={"users": []})

    with bigfoot:
        response = httpx.get("https://api.example.com/users")

    bigfoot.http.assert_request("GET", "https://api.example.com/users") \
        .assert_response(200, {"content-type": "application/json"}, '{"users": []}')

Enabling via constructor

Pass require_response=True when constructing the plugin manually:

from bigfoot import StrictVerifier
from bigfoot.plugins.http import HttpPlugin

verifier = StrictVerifier()
http = HttpPlugin(verifier, require_response=True)

Per-call override

The require_response parameter on assert_request() overrides both the constructor default and the project-level config:

# Force response assertion for this call, regardless of project config:
bigfoot.http.assert_request("GET", "https://api.example.com/data", require_response=True) \
    .assert_response(200, {}, '{"value": 42}')

# Disable response assertion for this call, even if project config enables it:
bigfoot.http.assert_request("GET", "https://api.example.com/health", require_response=False)

HttpAssertionBuilder

When require_response is active, assert_request() returns an HttpAssertionBuilder. This builder is lazy: it records the expected request fields but does not touch the timeline until assert_response() is called.

assert_response(status, headers, body) finalizes the assertion by calling verifier.assert_interaction() with all seven fields:

builder = bigfoot.http.assert_request("POST", "https://api.example.com/items",
                                       headers={"content-type": "application/json"},
                                       body='{"name": "widget"}',
                                       require_response=True)
builder.assert_response(201, {"content-type": "application/json"}, '{"id": 1}')

Configuration via pyproject.toml

See the Configuration Guide for full details on [tool.bigfoot.http].

Using with aiohttp

Requires bigfoot[aiohttp]. If aiohttp is not installed, HttpPlugin works normally for other transports.

import bigfoot, aiohttp

async def test_aiohttp_get():
    bigfoot.http.mock_response("GET", "https://api.example.com/data", json={"value": 42})

    async with bigfoot:
        async with aiohttp.ClientSession() as session:
            response = await session.get("https://api.example.com/data")
            assert response.status == 200
            body = await response.json()
            assert body == {"value": 42}

    bigfoot.http.assert_request("GET", "https://api.example.com/data",
                                headers={}, body="",
                                require_response=True) \
        .assert_response(200, {"content-type": "application/json"}, '{"value": 42}')

aiohttp POST with JSON body:

async def test_aiohttp_post():
    bigfoot.http.mock_response("POST", "https://api.example.com/items",
                               json={"id": 1}, status=201)

    async with bigfoot:
        async with aiohttp.ClientSession() as session:
            response = await session.post("https://api.example.com/items",
                                          json={"name": "widget"})
            assert response.status == 201

    bigfoot.http.assert_request("POST", "https://api.example.com/items",
                                headers={}, body='{"name": "widget"}',
                                require_response=True) \
        .assert_response(201, {"content-type": "application/json"}, '{"id": 1}')

The fake aiohttp response supports response.status, await response.json(), await response.text(), await response.read(), response.headers, and async with session.get(...) as response: context manager usage.

What HttpPlugin patches

When the sandbox activates, HttpPlugin installs class-level patches on:

  • httpx.HTTPTransport.handle_request (sync httpx)
  • httpx.AsyncHTTPTransport.handle_async_request (async httpx)
  • requests.adapters.HTTPAdapter.send (requests library)
  • urllib.request opener (urllib)
  • aiohttp.ClientSession._request (aiohttp, if installed)
  • asyncio.BaseEventLoop.run_in_executor (propagates ContextVar to thread pool executors)

All patches are reference-counted. Nested sandboxes increment/decrement the count; the actual method replacement only happens at count transitions from 0 to 1 and from 1 to 0.

The run_in_executor patch ensures the active-verifier ContextVar is copied into threads spawned by asyncio.run_in_executor, so HTTP calls made from thread pools are intercepted correctly.