Skip to main content

http-snapshot is a pytest plugin that snapshots requests made with popular Python HTTP clients.

Project description

http-snapshot

http-snapshot is a pytest plugin that captures and snapshots HTTP requests/responses made with popular Python HTTP clients like httpx and requests. It uses inline-snapshot to store HTTP interactions as JSON files, enabling fast and reliable HTTP testing without making actual network calls.

Features

  • 🚀 Support for multiple HTTP clients: httpx (async, sync) and requests (sync)
  • 📸 Automatic HTTP interaction capture: Records both requests and responses
  • 🔒 Security-aware: Automatically excludes sensitive headers like authorization and cookies
  • ⚙️ Configurable: Control what gets captured and what gets excluded
  • 🧪 pytest integration: Works seamlessly with your existing pytest test suite
  • 📁 External snapshots: Stores snapshots in organized JSON files

Installation

pip install http-snapshot

For specific HTTP client support:

# For httpx support
pip install http-snapshot[httpx]

# For requests support
pip install http-snapshot[requests]

# For both
pip install http-snapshot[httpx,requests]

Quick Start

Using Context Managers (Recommended)

The context manager API provides proper resource cleanup and doesn't require any additional dependencies.

Using with httpx (async)

import pytest
import inline_snapshot
from http_snapshot.httpx import HttpxAsyncSnapshotClient

@pytest.mark.anyio
@pytest.mark.parametrize(
    "http_snapshot",
    [inline_snapshot.external("uuid:my-test-snapshot.json")],
)
async def test_api_call(http_snapshot, is_recording: bool) -> None:
    async with HttpxAsyncSnapshotClient(
        snapshot=http_snapshot,
        is_recording=is_recording,
    ) as client:
        response = await client.get("https://api.example.com/users")
        assert response.status_code == 200
        assert "users" in response.json()

Using with httpx (sync)

import pytest
import inline_snapshot
from http_snapshot.httpx import HttpxSyncSnapshotClient

@pytest.mark.parametrize(
    "http_snapshot",
    [inline_snapshot.external("uuid:my-test-snapshot.json")],
)
def test_api_call(http_snapshot, is_recording: bool) -> None:
    with HttpxSyncSnapshotClient(
        snapshot=http_snapshot,
        is_recording=is_recording,
    ) as client:
        response = client.get("https://api.example.com/users")
        assert response.status_code == 200
        assert "users" in response.json()

Using with requests (sync)

import pytest
import inline_snapshot
from http_snapshot.requests import RequestsSnapshotSession

@pytest.mark.parametrize(
    "http_snapshot",
    [inline_snapshot.external("uuid:my-test-snapshot.json")],
)
def test_api_call(http_snapshot, is_recording: bool) -> None:
    with RequestsSnapshotSession(
        snapshot=http_snapshot,
        is_recording=is_recording,
    ) as session:
        response = session.get("https://api.example.com/users")
        assert response.status_code == 200
        assert "users" in response.json()

Using with pytest fixtures (Deprecated)

Note: The pytest fixture API is deprecated. Please use the context manager API shown above instead.

Using with httpx (async)

import httpx
import pytest
import inline_snapshot

@pytest.mark.anyio
@pytest.mark.parametrize(
    "http_snapshot",
    [inline_snapshot.external("uuid:my-test-snapshot.json")],
)
async def test_api_call(snapshot_async_httpx_client: httpx.AsyncClient) -> None:
    # This will be captured on first run, replayed on subsequent runs
    response = await snapshot_async_httpx_client.get("https://api.example.com/users")
    assert response.status_code == 200
    assert "users" in response.json()

Using with httpx (sync)

import httpx
import pytest
import inline_snapshot

@pytest.mark.parametrize(
    "http_snapshot",
    [inline_snapshot.external("uuid:my-test-snapshot.json")],
)
def test_api_call(snapshot_sync_httpx_client: httpx.Client) -> None:
    # This will be captured on first run, replayed on subsequent runs
    response = snapshot_sync_httpx_client.get("https://api.example.com/users")
    assert response.status_code == 200
    assert "users" in response.json()

Using with requests (sync)

import requests
import pytest
import inline_snapshot

@pytest.mark.parametrize(
    "http_snapshot",
    [inline_snapshot.external("uuid:my-test-snapshot.json")],
)
def test_api_call(snapshot_requests_session: requests.Session) -> None:
    # This will be captured on first run, replayed on subsequent runs
    response = snapshot_requests_session.get("https://api.example.com/users")
    assert response.status_code == 200
    assert "users" in response.json()

Migration Guide

If you're currently using the deprecated pytest fixtures, here's how to migrate to the context manager API:

Before (using fixtures):

@pytest.mark.anyio
@pytest.mark.parametrize(
    "http_snapshot",
    [inline_snapshot.external("uuid:test.json")],
)
async def test_api(snapshot_async_httpx_client: httpx.AsyncClient):
    await snapshot_async_httpx_client.get("https://example.com")

After (using context managers):

from http_snapshot.httpx import HttpxAsyncSnapshotClient

@pytest.mark.anyio
@pytest.mark.parametrize(
    "http_snapshot",
    [inline_snapshot.external("uuid:test.json")],
)
async def test_api(http_snapshot, is_recording: bool):
    async with HttpxAsyncSnapshotClient(
        snapshot=http_snapshot,
        is_recording=is_recording,
    ) as client:
        await client.get("https://example.com")

Key differences:

  1. Import the context manager: Instead of relying on pytest fixtures, import the context manager class
  2. Use context manager syntax: Use async with (for async) or with (for sync)
  3. Pass parameters explicitly: snapshot and is_recording are now constructor parameters
  4. Add is_recording fixture: The is_recording pytest fixture is still available and works the same way
  5. No additional dependencies needed: Unlike the fixtures which had async teardown issues, context managers work without pytest-asyncio

How It Works

# Record new HTTP interactions (makes actual network calls and creates snapshots)
pytest tests/ --http-record --inline-snapshot=create

# Re-record and update existing snapshots (makes actual network calls and updates snapshots)
pytest tests/ --http-record --inline-snapshot=fix

# Replay existing snapshots (default - no network calls made)
pytest tests/

Configuration Options

You can customize what gets captured using SnapshotSerializerOptions:

Using with context managers:

import pytest
import inline_snapshot
from http_snapshot.requests import RequestsSnapshotSession, SnapshotSerializerOptions

@pytest.mark.parametrize(
    "http_snapshot",
    [inline_snapshot.external("uuid:my-test-snapshot.json")],
)
def test_with_custom_options(http_snapshot, is_recording: bool) -> None:
    serializer_options = SnapshotSerializerOptions(
        exclude_request_headers=["X-API-Key"],
        include_request=True,
    )

    with RequestsSnapshotSession(
        snapshot=http_snapshot,
        is_recording=is_recording,
        serializer_options=serializer_options,
    ) as session:
        response = session.get(
            "https://api.example.com/protected",
            headers={"X-API-Key": "secret-key"}
        )
        assert response.status_code == 200

Using with fixtures (deprecated):

import pytest
import inline_snapshot
from http_snapshot import SnapshotSerializerOptions

@pytest.mark.parametrize(
    "http_snapshot, http_snapshot_serializer_options",
    [
        (
            inline_snapshot.external("uuid:my-test-snapshot.json"),
            SnapshotSerializerOptions(
                exclude_request_headers=["X-API-Key"],
                include_request=True,
            ),
        ),
    ],
)
def test_with_custom_options(
    snapshot_requests_session: requests.Session,
    http_snapshot_serializer_options: SnapshotSerializerOptions,
) -> None:
    response = snapshot_requests_session.get(
        "https://api.example.com/protected",
        headers={"X-API-Key": "secret-key"}
    )
    assert response.status_code == 200

Available Options

  • include_request: Whether to include request details in snapshots (default: True)
  • exclude_request_headers: List of request headers to exclude from snapshots
  • exclude_response_headers: List of response headers to exclude from snapshots

By default, the following sensitive headers are always excluded:

  • Request: authorization, cookie
  • Response: set-cookie, www-authenticate, proxy-authenticate, authentication-info, proxy-authentication-info, transfer-encoding, content-encoding

Snapshot Format

Snapshots are stored as JSON files with the following structure:

[
  {
    "request": {
      "method": "GET",
      "url": "https://api.example.com/users",
      "headers": {
        "host": "api.example.com",
        "accept": "*/*",
        "accept-encoding": "gzip, deflate",
        "connection": "keep-alive",
        "user-agent": "python-httpx/0.28.1"
      },
      "body": ""
    },
    "response": {
      "status_code": 200,
      "headers": {
        "date": "Thu, 21 Aug 2025 15:49:45 GMT",
        "content-type": "application/json; charset=utf-8",
        "connection": "keep-alive",
        "server": "nginx/1.18.0"
      },
      "body": {
        "users": [
          {
            "id": 1,
            "name": "John Doe",
            "email": "john@example.com"
          },
          {
            "id": 2,
            "name": "Jane Smith",
            "email": "jane@example.com"
          }
        ]
      }
    }
  }
]

Content Encoding

The plugin intelligently handles different content types:

  • JSON: Formatted with proper indentation for readability
  • Text: Stored as UTF-8 strings
  • Binary: Base64 encoded

Advanced Examples

Testing API with Multiple Requests

from http_snapshot.httpx import HttpxAsyncSnapshotClient

@pytest.mark.anyio
@pytest.mark.parametrize(
    "http_snapshot",
    [inline_snapshot.external("uuid:multi-request-test.json")],
)
async def test_multiple_requests(http_snapshot, is_recording: bool) -> None:
    async with HttpxAsyncSnapshotClient(
        snapshot=http_snapshot,
        is_recording=is_recording,
    ) as client:
        create_response = await client.post(
            "https://api.example.com/users",
            json={"name": "Alice", "email": "alice@example.com"}
        )
        assert create_response.status_code == 201
        user_id = create_response.json()["id"]

        get_response = await client.get(
            f"https://api.example.com/users/{user_id}"
        )
        assert get_response.status_code == 200
        assert get_response.json()["name"] == "Alice"

Testing with Authentication

from http_snapshot.requests import RequestsSnapshotSession, SnapshotSerializerOptions

@pytest.mark.parametrize(
    "http_snapshot",
    [inline_snapshot.external("uuid:auth-test.json")],
)
def test_authenticated_request(http_snapshot, is_recording: bool) -> None:
    serializer_options = SnapshotSerializerOptions(
        exclude_request_headers=["Authorization"]
    )

    with RequestsSnapshotSession(
        snapshot=http_snapshot,
        is_recording=is_recording,
        serializer_options=serializer_options,
    ) as session:
        response = session.get(
            "https://api.example.com/profile",
            headers={"Authorization": "Bearer secret-token"}
        )
        assert response.status_code == 200

Best Practices

  1. Exclude sensitive data: Always exclude headers containing secrets, tokens, or personal data
  2. Review snapshots: Check generated snapshot files into version control and review changes

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

http_snapshot-0.1.8.tar.gz (13.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

http_snapshot-0.1.8-py3-none-any.whl (14.1 kB view details)

Uploaded Python 3

File details

Details for the file http_snapshot-0.1.8.tar.gz.

File metadata

  • Download URL: http_snapshot-0.1.8.tar.gz
  • Upload date:
  • Size: 13.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for http_snapshot-0.1.8.tar.gz
Algorithm Hash digest
SHA256 1f0dc437fea4e13179f26290216bb402096a44f3b29969022a78c2e71fddfc28
MD5 6f45a475ded6e3aa6988939a6d4ed979
BLAKE2b-256 37a4bd9cea69146855b47aefec010f36b99741208e5f2dcbf522e44f0f66ce2a

See more details on using hashes here.

Provenance

The following attestation bundles were made for http_snapshot-0.1.8.tar.gz:

Publisher: publish.yml on karpetrosyan/http-snapshot

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file http_snapshot-0.1.8-py3-none-any.whl.

File metadata

  • Download URL: http_snapshot-0.1.8-py3-none-any.whl
  • Upload date:
  • Size: 14.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for http_snapshot-0.1.8-py3-none-any.whl
Algorithm Hash digest
SHA256 5d10e9cad9fe26fda8d1ebcd2adb66589dd3fd0fd58e7960c61c08c483d31457
MD5 d2cdb3324bd9eb9b642b41ace49082a1
BLAKE2b-256 26197b2a6ccded7bf19a23c5512f08a6042d5900b3d9d9b88708baf08f9abd80

See more details on using hashes here.

Provenance

The following attestation bundles were made for http_snapshot-0.1.8-py3-none-any.whl:

Publisher: publish.yml on karpetrosyan/http-snapshot

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page