HTTP client
English | Русский
Installation
Requires Python 3.13 or later.
pip install cloudru-qa-http-client
Import the library as the http_client module.
Skills for AI agents
The package includes the http-client-usage skill
with examples and usage rules for AI agents. Install the library
in your project environment, then run this from its root (requires uv):
uvx library-skills install --skill http-client-usage --yes
For Claude Code, add --claude. The instructions are shipped with the library version.
Usage
The library provides HttpClient and AsyncHttpClient. Use them directly or subclass them to create custom clients.
All options (logging, request identification, tracing, default headers, and others) work the same for both clients. Synchronous event hooks are automatically wrapped as async when used with AsyncHttpClient.
Example:
Base client example
import logging
from http_client import HttpClient, options
from httpx import BasicAuth
with HttpClient(
"https://api.example.com",
options.with_auth(BasicAuth("demo", "example-password")),
options.with_logger(logging.getLogger(__name__)),
) as client:
response = client.post("/test_method", json={"test": True})
response.raise_for_status()
Custom client example
import logging
from http_client import HttpClient, options
from httpx import Response
class CustomHttpClient(HttpClient):
def test_method(self, data: dict[str, object]) -> Response:
"""Send data to a service method.
:param data: JSON request body.
:return: service response.
"""
request = self.client.build_request("POST", "/test_method", json=data)
return self.send(request)
with CustomHttpClient(
"https://api.example.com",
options.with_logger(logging.getLogger(__name__)),
options.with_autogenerated_request_id(),
options.with_autogenerated_traceparent(),
) as client:
response = client.test_method({"test": True})
response.raise_for_status()
Asynchronous client example
All standard options, including synchronous event hooks, work unchanged with AsyncHttpClient:
import logging
from http_client import AsyncHttpClient, options
async with AsyncHttpClient(
"https://api.example.com",
options.with_logger(logging.getLogger(__name__)),
options.with_autogenerated_request_id(),
) as client:
response = await client.post("/test_method", json={"test": True})
response.raise_for_status()
Configuration
Configure the client with ClientOption options. Each option receives the actual client instance and runs in its constructor. Register hooks and decorators with add_request_hook, add_response_hook, add_send_decorator, and add_stream_decorator:
from http_client import BaseHttpClient, ClientOption
from httpx import Request
def with_region(region: str) -> ClientOption:
"""Add a region to outgoing request headers.
:param region: region identifier.
:return: client option.
"""
def add_region(request: Request) -> None:
request.headers["X-Region"] = region
def option(client: BaseHttpClient) -> None:
client.add_request_hook(add_region, prepend=True)
return option
Registration is available while constructor options run. After client creation, its transport is already configured.
Available options
The options module provides options for configuring the HTTP client. Combine them when creating a client instance.
EventHooks
Event hook options let you process requests and responses:
with_request_hook— adds a function to process requests before sendingwith_response_hook— adds a function to process responses after receiving
These options provide a flexible way to handle each client request and/or response.
Usage example:
import logging
from httpx import Request, Response
from http_client import HttpClient, options
def my_request_hook(request: Request) -> None:
# process the request before sending it
request.headers["X-Custom-Header"] = "Custom Value"
def my_response_hook(response: Response) -> None:
# process the response after receiving it
logging.getLogger(__name__).info("Response status code: %s", response.status_code)
client = HttpClient(
"http://example.com",
options.with_request_hook(my_request_hook),
options.with_response_hook(my_response_hook),
)
With AsyncHttpClient, synchronous hooks are automatically wrapped as async. You can also pass native async hooks:
from http_client import AsyncHttpClient, options
from httpx import Request
async def my_async_request_hook(request: Request) -> None:
request.headers["X-Async"] = "true"
client = AsyncHttpClient(
"http://example.com",
options.with_request_hook(my_async_request_hook),
)
See the httpx documentation for more about hooks.
Decorating the send method
The HTTP client supports decorating its send method with with_send_decorator. This lets you add behavior before sending the HTTP request and after receiving the response.
with_send_decorator applies the decorator pattern to send, letting you:
- Preprocess the request
- Postprocess the response
- Implement retries, logging, caching, monitoring, and other features
Example of a request logging decorator:
from functools import wraps
from http_client import HttpClient, options
from http_client.types import SendOptions, SyncSendMethod
from typing import Unpack
import logging
import httpx
def logging_decorator(func: SyncSendMethod) -> SyncSendMethod:
@wraps(func)
def wrapper(request: httpx.Request, **kwargs: Unpack[SendOptions]) -> httpx.Response:
logging.getLogger(__name__).info("Sending request: %s %s", request.method, request.url)
response = func(request, **kwargs)
logging.getLogger(__name__).info("Received response: %s", response.status_code)
return response
return wrapper
client = HttpClient("https://example.com", options.with_send_decorator(logging_decorator))
For the asynchronous HTTP client, the decorator must be asynchronous:
from functools import wraps
from http_client import AsyncHttpClient, options
from http_client.types import AsyncSendMethod, SendOptions
from typing import Unpack
import logging
import httpx
def async_logging_decorator(func: AsyncSendMethod) -> AsyncSendMethod:
@wraps(func)
async def wrapper(request: httpx.Request, **kwargs: Unpack[SendOptions]) -> httpx.Response:
logging.getLogger(__name__).info("Sending request: %s %s", request.method, request.url)
response = await func(request, **kwargs)
logging.getLogger(__name__).info("Received response: %s", response.status_code)
return response
return wrapper
client = AsyncHttpClient("https://example.com", options.with_send_decorator(async_logging_decorator))
You can apply several decorators. Each new decorator wraps the previous one, so the last one added is outermost and runs first:
client = HttpClient(
"https://example.com",
options.with_send_decorator(decorator1), # applied first, becomes innermost
options.with_send_decorator(decorator2), # applied second, becomes outermost
)
Request calls run in this order: decorator2 → decorator1 → send. The response passes through them in reverse: decorator1 finishes first, then decorator2.
Streaming requests
The HTTP client supports streaming requests in two ways:
The stream method
stream(method, url, **kwargs) is fully compatible with the httpx API. It takes an HTTP method and URL, builds a request, and returns a context manager:
from http_client import HttpClient, options
client = HttpClient(
"https://example.com",
options.with_auth(auth),
options.with_logger(logger),
)
# Streaming download (httpx contract)
with client.stream("GET", "/large-file.zip") as response:
response.raise_for_status()
with open("file.zip", "wb") as f:
for chunk in response.iter_bytes(chunk_size=8192):
f.write(chunk)
Asynchronous client example:
from http_client import AsyncHttpClient, options
client = AsyncHttpClient(
"https://example.com",
options.with_auth(auth),
options.with_logger(logger),
)
async with client.stream("GET", "/stream-data") as response:
response.raise_for_status()
async for line in response.aiter_lines():
process_line(line)
The stream_request method
stream_request(request, **kwargs) takes a ready-made httpx.Request and supports all client options, including decorators:
request = client.client.build_request("GET", "/large-file.zip")
with client.stream_request(request) as response:
for chunk in response.iter_bytes(chunk_size=8192):
f.write(chunk)
Decorating the stream_request method
Like send, stream_request can be decorated with with_stream_decorator:
from functools import wraps
from contextlib import contextmanager
from http_client import HttpClient, options
from http_client.types import StreamOptions, SyncStreamRequestMethod
from collections.abc import Iterator
from typing import Unpack
import httpx
import logging
def stream_logging_decorator(func: SyncStreamRequestMethod) -> SyncStreamRequestMethod:
@wraps(func)
@contextmanager
def wrapper(request: httpx.Request, **kwargs: Unpack[StreamOptions]) -> Iterator[httpx.Response]:
logging.getLogger(__name__).info("Starting stream: %s %s", request.method, request.url)
with func(request, **kwargs) as response:
logging.getLogger(__name__).info("Stream status: %s", response.status_code)
yield response
logging.getLogger(__name__).info("Stream finished")
return wrapper
client = HttpClient("https://example.com", options.with_stream_decorator(stream_logging_decorator))
Important: When logging streaming requests, the response body appears as <streamed content> because it has not yet been read when the response hook runs.
The request method
request(method, url, **kwargs) sends HTTP requests with any method:
response = client.request("PATCH", "/api/resource", json={"field": "value"})
Timeouts
with_timeout sets different types of HTTP request timeouts:
from httpx import Timeout
from http_client import HttpClient, options
# Set different timeout types
timeout = Timeout(
connect=5.0, # time to establish a connection
read=10.0, # time to read the response
write=5.0, # time to write the request
pool=1.0, # time to wait for a free connection in the pool
)
client = HttpClient("https://example.com", options.with_timeout(timeout))
Authorization
with_auth configures HTTP request authorization. All httpx authorization mechanisms are supported. You can also implement your own; see the httpx documentation.
from httpx import BasicAuth, DigestAuth
from http_client import HttpClient, options
# Basic authorization
client = HttpClient("https://example.com", options.with_auth(BasicAuth(username="user", password="password")))
# Digest authorization
client = HttpClient("https://example.com", options.with_auth(DigestAuth(username="user", password="password")))
# Custom Bearer token authorization
client = HttpClient(
"https://example.com", options.with_auth(lambda r: r.headers.update({"Authorization": f"Bearer {token}"}))
)
Request identification
with_autogenerated_request_id automatically adds a unique identifier to each request's X-Request-ID header:
from http_client import HttpClient, options
# Automatically generate X-Request-ID
client = HttpClient("https://example.com", options.with_autogenerated_request_id())
This is useful for tracing requests across microservices and for debugging.
Tracing
with_autogenerated_traceparent automatically adds a traceparent tracing header compatible with W3C Trace Context:
from http_client import HttpClient, options
# Add a traceparent header to every request
client = HttpClient("https://example.com", options.with_autogenerated_traceparent())
This lets you track requests across multiple services using different monitoring systems.
Logging
with_logger configures HTTP request and response logging. Set shared defaults with a LoggingConfig object:
from http_client import configure_logging
from http_client.config import LoggingConfig
configure_logging(options=LoggingConfig(json_indent=2, use_line_breaks=True))
Pass client-specific settings to with_logger:
import logging
from http_client import HttpClient, options
# Configure a custom logger
logger = logging.getLogger("http_client")
client = HttpClient(
"https://example.com",
options.with_logger(
logger,
sanitize_keys=["authorization", "password"], # hide secret data
),
)
sanitize_keys hides sensitive information in logs, such as authorization tokens and passwords.
Common secrets are masked by default, even without sanitize_keys: authorization, proxy-authorization, cookie, set-cookie, x-api-key, api-key, api_key, access_token, refresh_token, password, secret, token. Keys from sanitize_keys extend this list.
Disable the default protection with an explicit flag, for example when debugging in a private environment:
options.with_logger(logger, use_default_sanitize_keys=False)
⚠️ Masking applies only to access log records written by this library. httpx exceptions may contain the full URL, including query parameters, so do not pass secrets in URLs.
Closing connections
The connection pool is created in the client constructor, so close the client. A context manager is the simplest way:
with HttpClient("https://example.com") as client:
client.get("/api/resource")
async with AsyncHttpClient("https://example.com") as client:
await client.get("/api/resource")
For a long-lived client created once per application, use explicit methods:
client.close() # HttpClient
await client.aclose() # AsyncHttpClient
A closed client cannot be reopened: like httpx, this raises RuntimeError.
TLS connection
The library provides two options to configure TLS connections. Both work the same with HttpClient and AsyncHttpClient.
with_verify_context— sets the TLS verification context:
import ssl
from http_client import HttpClient, options
# Create an SSL context
ssl_context = ssl.create_default_context()
ssl_context.load_verify_locations("/path/to/ca-certificates.crt")
# Set the SSL context
client = HttpClient("https://example.com", options.with_verify_context(verify=ssl_context))
use_truststore— uses the system certificate store for verification:
from http_client import HttpClient, options
# Use the system certificate store
client = HttpClient("https://example.com", options.use_truststore())
To use use_truststore, install the extra dependency:
pip install "cloudru-qa-http-client[truststore]"
Default headers
with_default_headers sets default headers sent with all client requests.
from http_client import HttpClient, options
headers = {
"X-My-Custom-Header": "Hello, world!",
"X-Another-Header": "Another value",
}
client = HttpClient(
"https://example.com",
options.with_default_headers(headers),
)
Development
The library is developed in the qa-libraries monorepo
alongside the other cloudru-qa-* libraries. You need uv 0.11.14
or later, GNU Make, and checkmake. Run commands from the repository root:
make sync # install dependencies for all libraries from uv.lock
make test-http-client # tests for this library
make typecheck-http-client # mypy for this library
make check # formatting, ruff, mypy, and tests for the entire workspace
⚠️ Keep the library lightweight and general-purpose. Discuss new dependencies only when you are sure all library users need them.
Tests run on Python 3.13 and 3.14. Create a separate environment for another version:
make sync test-http-client PYTHON_VERSION=3.14.
Demo scenarios with real requests to local go-httpbin
are in examples/httpbin. Docker must be running; run commands from packages/http-client:
docker compose up -d
uv run --no-sync python -m examples.httpbin
docker compose down
Publish a release with the git tag http-client/v<version>. The tag version must match the version
in packages/http-client/pyproject.toml.
Metadata
Release files for cloudru-qa-http-client 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| cloudru_qa_http_client-1.0.0.tar.gz | 47.8 kB | With provenance | Details for cloudru_qa_http_client-1.0.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| cloudru_qa_http_client-1.0.0-py3-none-any.whl | Python 3 | none | any | Details for cloudru_qa_http_client-1.0.0-py3-none-any.whl |
Total release size: 84.2 kB
Release files / cloudru_qa_http_client-1.0.0.tar.gz
| Download URL | cloudru_qa_http_client-1.0.0.tar.gz |
|---|---|
| Size | 47.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
401e6f9fd4a12b8f0cd128d5dbe23d1598b9c8d7072aea4e247f3fba7e5ea854
|
|
BLAKE2b-256 checksum How to use checksums |
4265985b05e9d2a241814ce54d0a49b31a18cf3d45ec8b6671c50902b6e5c1a5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 Nov 14, 2026.
Transparency logRelease files / cloudru_qa_http_client-1.0.0-py3-none-any.whl
| Download URL | cloudru_qa_http_client-1.0.0-py3-none-any.whl |
|---|---|
| Size | 36.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f1b3d5c2e3129f6b479c0ad114cefd477770d166530b17afaea84f26d131f671
|
|
BLAKE2b-256 checksum How to use checksums |
53ce0cc0931493c1a7b740146bf6aad66c684b5af3eb2ce580851f19edbf45bf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 Nov 14, 2026.
Transparency logSecurity for cloudru-qa-http-client 1.0.0
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.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for cloudru_qa_http_client-1.0.0.tar.gz
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 Nov 14, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.