pytest-httpchain
A pytest plugin for testing HTTP endpoints.
Overview
pytest-httpchain is an integration testing framework for HTTP APIs based on httpx lib.
It aims at helping with common HTTP API testing scenarios, where user needs to make several calls in specific order using data obtained along the way, like auth tokens or resource ids.
Why pytest-httpchain?
Testing HTTP APIs with plain pytest often leads to these pain points:
- Boilerplate accumulates — Every test repeats the same setup: create client, set headers, make request, parse response, assert. The actual test intent gets buried.
- Data threading is manual — When one call returns a token or ID needed by the next, you end up with fragile helper functions passing state around.
- Common patterns get copy-pasted — Auth flows, base URLs, shared headers end up duplicated across test files. Fixtures might help, but they are not designed for that.
- Code reviews are noisy — The actual test logic is rarely clear because of all the boilerplate and helpers, following changes gets overwhelming quickly.
pytest-httpchain offers a more structured approach.
Features
Declarative JSON format
Test scenarios are JSON documents that describe what to test, not how. No setup code to scroll through — the request and assertions are right there. Comments (//, /* */) and trailing commas are welcome in every scenario and every file it pulls in; name a file test_<name>.http.jsonc and editors treat it as JSON with comments.
$include / $merge with deep merging
Reuse arbitrary parts of your scenarios with JSONRef. Properties merge with type checking, so you can compose scenarios from shared fragments (auth flows, common headers, base URLs). $ref is a legacy alias that still works, but prefer $include/$merge: VS Code gives $ref its own JSON Schema handling, which fights the editor integration below.
Multi-stage execution
Each scenario contains 1+ stages executed in order. One stage failure stops the chain. Use always_run for cleanup stages that should execute regardless, and skip_if to skip a stage on a condition known only once the chain is running, such as a value an earlier stage saved.
Retries and polling
A stage's retry attempts it again while it fails, after a wait that can grow each time: poll an asynchronous job until it reports done, or ride out eventual consistency and a flaky network ("retry": {"attempts": 10, "delay": 0.5, "backoff": 2}). Each attempt sends a freshly rendered request and runs every response step; only the attempt that passes saves anything.
Parallel stages and load checks
A stage's parallel sends its request many times at once (repeat) or once per parameter set (foreach), with a concurrency cap and a rate limit. Its report sums the run up: iterations passed, failed and cancelled, wall time, throughput and p50/p95/p99 latency. thresholds fail the stage below a success ratio or above a latency ("thresholds": {"min_success_ratio": 0.99, "max_p95_ms": 300}), letting it run on after failed requests as long as enough pass, and stats_as saves the numbers for a later stage.
Common data context
A key-value store persists throughout scenario execution. Variables, fixtures, and saved response data all live here. Use template expressions ({{ var }}) in any request value — substitution happens dynamically before each stage. (Dict keys are not substituted; HTTPCHAIN029 flags a template in a key.) Built-in functions give the values tests keep needing without a fixture: the time (now(), timestamp()), base64, JSON and URL encoding, and SHA-256, MD5 and HMAC-SHA256 digests for signing a request.
Request bodies
JSON, form, XML, text, base64, a binary file and GraphQL, and multipart uploads that mix form fields with files: each file read from a path or given inline, several under one name if need be, with its own filename and content type.
Response processing
- JMESPath — Assert on values in JSON responses directly (
"jmespath": {"data.id": 42, "items": {"length": 3}}), or extract them for later stages - Regex — Save values from bodies that are not JSON, such as a CSRF token from an HTML form (
"regex": {"csrf": "name=\"csrf\" value=\"([^\"]+)\""}) - JSON Schema — Validate response structure against a schema, inline or from a file, or one inside a document you already have:
"schema": "./openapi.json#/components/schemas/User"checks the response against an OpenAPI component, its$refs resolved across the document and into local files, never over the network - User functions — Call Python functions for custom extraction, verification, or authentication
- Failure reports — A failing verify step lists every check that failed, not only the first, and the report gives the request as a ready-to-run
curlcommand beside the request and response it shows
Scenario-wide client settings
A scenario's client block sets up the HTTP client all its stages share, once: a base URL their relative URLs are appended to, headers and query parameters sent with every request, timeout, redirects, proxy, HTTP/2 and connection pool. A stage overrides what it needs.
Authentication
Basic, digest and bearer authentication are built in, for the whole scenario or one request: "auth": {"bearer": "{{ token }}"} sends the token a login stage saved. "auth": false exempts a public endpoint, and a Python function covers any other scheme.
Import recorded traffic
Start from traffic you already have: pytest-httpchain import har session.har turns a browser's HAR export into a scenario, a stage per request, and pytest-httpchain import curl '...' does the same for curl commands from an API's docs or a failing stage's report. It sets the base URL, maps query strings, JSON, form and multipart bodies and Basic or Bearer credentials into the dialect, leaves out the transport headers and static assets, and writes no secret: tokens, passwords and cookies become placeholders read from environment variables. What it writes passes validate.
Full pytest integration
Markers, fixtures, parametrization, and other plugins work as expected. You're not locked into a separate ecosystem.
Quick Start
Create a JSON test file named like test_<name>.<suffix>.json (default suffix is http):
{
"client": {
"base_url": "https://api.example.com"
},
"substitutions": [
{
"vars": {
"user_id": 1
}
}
],
"stages": {
"get_user": {
"request": {
"url": "/users/{{ user_id }}"
},
"response": [
{
"verify": {
"status": 200
}
},
{
"save": {
"jmespath": {
"user_name": "user.name"
}
}
}
]
},
"update_user": {
"request": {
"url": "/users/{{ user_id }}",
"method": "PUT",
"body": {
"json": {
"user": {
"name": "{{ user_name }}_updated",
"timestamp": "{{ now() }}"
}
}
}
},
"response": [
{
"verify": {
"status": 200
}
}
]
},
"cleanup": {
"always_run": true,
"request": {
"url": "/cleanup",
"method": "POST"
}
}
}
}
Scenario we created:
- the scenario's HTTP client gets a base URL, so every stage gives only its path
- common data context is seeded with the first variable
user_id - get_user
url is assembled usinguser_idvariable from common data context, and appended to the base URL
HTTP GET call is made
we verify the call returned code 200
assuming JSON body is returned, we extract a value by JMESPath expressionuser.nameand save it to common data context underuser_namekey - update_user
url is assembled usinguser_idvariable from common data context
we create JSON body in place using values from common data context, and the current UTC time from the built-innow()
HTTP PUT call with body is made
we verify the call returned code 200 - cleanup
finalizing call meant for graceful exit
always_runparameter means this stage will be executed regardless of errors in previous stages
For detailed usage guide see the full documentation, and the CLI reference for the offline authoring commands.
Installation
Install normally via package manager of your choice from PyPi:
pip install pytest-httpchain
or directly from Github, in case you need a particular ref:
pip install 'git+https://github.com/aeresov/pytest-httpchain@main'
Configuration
- Test file discovery is based on this name pattern:
test_<name>.<suffix>.json, ortest_<name>.<suffix>.jsonc. The suffix is configurable via thehttpchain_suffixpytest ini option, default value is http. $include/$mergeinstructions (and their legacy alias$ref) can point to other files using relative paths; absolute paths are rejected for security, and every reference must resolve inside the root path (pytest'srootdirwhen collecting;--root-pathfor the CLI). You can limit the depth of relative path traversal using thehttpchain_ref_parent_traversal_depthini option, default value is 3.- Template expressions support list/dict comprehensions. You can limit the maximum comprehension length using the
httpchain_max_comprehension_lengthini option, default value is 50000. - Parallel stage iterations (repeat/foreach) have a safety limit configurable via the
httpchain_max_parallel_iterationsini option, default value is 10000. - A failing stage's report prints the values of credential headers and query parameters as
[REDACTED], keeping their names (and cookie names). The lists are set by thehttpchain_redact_headersini option, default Authorization Proxy-Authorization Cookie Set-Cookie X-API-Key API-Key X-Auth-Token, andhttpchain_redact_query_params, default access_token refresh_token id_token api_key apikey client_secret password token; an empty value disables one. Request and response bodies, and DEBUG logs, are not redacted. See Secrets in reports.
HAR export
Pass --httpchain-output-dir DIR on the pytest command line to write an HAR file (and a "HAR File" report section) capturing each test's HTTP traffic:
pytest --httpchain-output-dir ./har-output
HAR files contain full requests/responses including credential headers and saved tokens: a HAR is usually replayed, which needs the real values, so nothing is redacted unless the httpchain_har_redact ini option is true (it applies the report's rules to URLs, headers and cookies; bodies stay complete). Scrub them before sharing. Bodies are embedded complete and uncapped (binary bodies grow ~33% as base64), so scenarios that transfer large payloads produce large .har files. See the HAR export docs.
AI agent support
pytest-httpchain ships a scenario validator to help AI coding agents (and humans) author and check test scenarios.
Scenario validation
Validate scenario files for structure and common problems — undefined variables, variables referenced before they are saved (data-flow ordering), duplicate stage names, fixture/variable conflicts, no-op verify steps, and contradictory body checks. Name the files, or a directory to check every scenario pytest would collect in it:
uvx pytest-httpchain validate tests/test_login.http.json
uvx pytest-httpchain validate tests/
Each finding carries a stable diagnostic code (HTTPCHAINxxx) and a severity — the full code reference is on the docs site, along with a recipe for filtering the ScenarioValidationWarning warnings the same checks emit at pytest collection. It exits non-zero when any file is invalid, so it doubles as a CI gate. Use --format json for machine-readable output (editor/CI integration):
uvx pytest-httpchain validate --format json tests/test_login.http.json
The same checks also run automatically at pytest collection time — semantic errors fail collection and warnings are reported — so pytest --collect-only validates every scenario in your suite.
For deeper, opt-in checks, add --deep: it imports your module:func references to confirm they resolve, checks their call signatures (including the injected response for save/verify functions), and verifies referenced files and schemas exist. Because it imports your code it is never run at collection time; pair it with --strict to fail CI on any warning, and --syspath to add import roots:
uvx pytest-httpchain validate --deep --strict tests/test_login.http.json
Editor schema
A JSON Schema is published for as-you-type validation and autocomplete. Reference it from your test files:
{
"$schema": "https://aeresov.github.io/pytest-httpchain/schema/scenario.schema.json"
}
The hosted schema at the unversioned URL tracks the main branch (it is redeployed on every push, so it may describe unreleased changes). To pin the schema for a release, use its versioned URL:
{
"$schema": "https://aeresov.github.io/pytest-httpchain/schema/v0.14.0/scenario.schema.json"
}
or emit the schema matching your installed version locally:
uvx pytest-httpchain schema > scenario.schema.json
Inspecting scenarios
More read-only commands help author and debug scenarios offline — no network, no test run:
# Print a scenario with all $ref/$include/$merge inlined and deep-merged, as strict JSON (comments dropped)
uvx pytest-httpchain resolve tests/test_login.http.json
# Summarize stages and the variable data-flow (which stage saves what, who consumes it)
uvx pytest-httpchain show tests/test_login.http.json
# Render the stage data-flow as a Mermaid flowchart
uvx pytest-httpchain graph tests/test_login.http.json
Documentation
- Full Documentation - Complete usage guide
- Changelog - Release notes
Thanks
This project was inspired by Tavern and pytest-play.
httpx does comms.
Pydantic keeps structure.
simpleeval powers templates.
pytest-datadir saved me a lot of elbow grease while testing.
Metadata
Release files for pytest-httpchain 0.17.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pytest_httpchain-0.17.1.tar.gz | 269.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pytest_httpchain-0.17.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 557.4 kB
Release files / pytest_httpchain-0.17.1.tar.gz
| Download URL | pytest_httpchain-0.17.1.tar.gz |
|---|---|
| Size | 269.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
beafbfd376be9d99c4b76571a44f9bfd84a728d63e8eb81472c2a7f856898239
|
|
BLAKE2b-256 checksum How to use checksums |
d82babf78ad551c1e2428f19f10a162147cdfc8b1bc46b68347b27286de4e88c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.
Transparency logRelease files / pytest_httpchain-0.17.1-py3-none-any.whl
| Download URL | pytest_httpchain-0.17.1-py3-none-any.whl |
|---|---|
| Size | 288.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c62cd39e2579a89749b38de7c5f6576613474b74072fdbf26621154bec2b4499
|
|
BLAKE2b-256 checksum How to use checksums |
5eb41bdd4f72994f8d2e7ddea2f1f6f4e609c39461bfeb2ce6de61845e0262c3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.
Transparency log