Middleware¶
Middleware lets you customize request handling without changing the code that makes HTTP requests.
OxideHTTP supports middleware configured globally through RequestConfig or individually for a specific request.
Registering Middleware¶
Register middleware globally to apply it to requests made by a client:
from oxidehttp import OxideHTTP
from oxidehttp.config import OxideHTTPConfig, RequestConfig
config = OxideHTTPConfig(
request=RequestConfig(
middlewares=(
# Middleware instances
)
)
)
async with OxideHTTP(config) as client:
response = await client.get('/users')
You can also pass middleware to an individual request:
This is useful when middleware should only affect selected requests.
Middleware Interface¶
Middleware implementations expose an asynchronous run method that receives the request and the next middleware in the chain.
Conceptually, the method has this interface:
async def run(self, request: Request, next_req: Next) -> Response:
response = await next_req.run(request)
return response
A middleware can inspect or modify a request before forwarding it, and inspect or modify the response afterward.
The Request, Response, and Next types come from the underlying pyreqwest middleware interface.
RetryMiddleware¶
RetryMiddleware retries requests when certain transport errors or HTTP status codes occur.
Basic Usage¶
from oxidehttp.middlewares.retry.middleware import RetryMiddleware
retry = RetryMiddleware()
config = OxideHTTPConfig(request=RequestConfig(middlewares=(retry,)))
Options¶
| Option | Type | Default | Description |
|---|---|---|---|
max_attempts |
int |
3 |
Maximum total number of attempts, including the initial request. |
backoff_factor |
float |
0.5 |
Base delay used for exponential backoff. |
max_delay |
float |
30.0 |
Maximum exponential backoff delay, in seconds. |
respect_retry_after |
bool |
True |
Whether to respect the response's Retry-After header. |
max_retry_after |
float \| None |
None |
Optional maximum delay from Retry-After, in seconds. |
jitter |
bool |
True |
Randomizes exponential backoff delays. |
status_codes |
Iterable[int] |
See below | HTTP status codes that trigger retries. |
retry_methods |
Iterable[str] |
See below | HTTP methods eligible for retries. |
logger |
Logger \| None |
None |
Logger used for retry messages. |
The default retry status codes are:
The default retry methods are:
Method names are compared directly, so use uppercase HTTP method names.
Maximum Attempts¶
max_attempts includes the initial request.
For example:
The request can be executed up to three times: one initial attempt and up to two retries.
max_attempts must be at least 1.
Exponential Backoff¶
The exponential backoff delay is calculated using:
For example:
Without jitter, the delays before subsequent attempts are:
| Attempt | Delay |
|---|---|
| 2 | 0.5 seconds |
| 3 | 1 second |
| 4 | 2 seconds |
| 5 | 4 seconds |
The maximum exponential delay is capped by max_delay.
Jitter¶
Jitter is enabled by default.
Instead of always waiting for the full exponential backoff delay, OxideHTTP chooses a random delay between zero and the calculated delay, with millisecond resolution.
This helps prevent multiple clients from retrying simultaneously.
Disable it when deterministic delays are preferred:
Retry-After¶
When respect_retry_after is enabled, the middleware checks the response's Retry-After header.
It supports:
- Integer delay values, representing seconds.
- HTTP-date values, representing the time when the client should retry.
For example:
Or:
By default, the middleware does not impose a maximum delay on a valid Retry-After value.
Use max_retry_after to cap it:
This option is independent of max_delay, which applies to exponential backoff.
Retry Conditions¶
A request is retried when:
- The maximum number of attempts has not been reached.
- Its HTTP method is included in
retry_methods. - The response status is included in
status_codes.
Transport errors raised as PyreqwestError are also retried, provided another attempt is available.
Other exceptions are not caught by the retry middleware.
Important Considerations¶
Retrying requests can cause duplicate operations if the server processes a request but the client fails to receive its response.
The default retry methods avoid automatically retrying POST and PATCH. Add other methods only when their operations are safe to retry or protected by an idempotency mechanism.
RequestIDMiddleware¶
RequestIDMiddleware generates or propagates a request identifier.
It adds the identifier to the outgoing request headers and, after receiving a response, adds the same identifier to the response headers.
Basic Usage¶
from oxidehttp.middlewares.requestid.middleware import RequestIDMiddleware
request_id = RequestIDMiddleware()
config = OxideHTTPConfig(request=RequestConfig(middlewares=(request_id,)))
By default, identifiers are generated using UUID4.
The default header name is:
Options¶
| Option | Type | Default | Description |
|---|---|---|---|
request_id_header |
str \| None |
None |
Header used to propagate the request ID. Defaults to X-Request-ID. |
request_id_factory |
Callable[[], str] \| None |
None |
Function used to generate request IDs. Defaults to UUID4 generation. |
logger |
Logger \| None |
None |
Logger used for generated request IDs. |
Custom Header¶
Change the header name when integrating with an API that uses a different request ID convention:
Custom Request ID Factory¶
Provide your own function to generate request IDs:
from uuid import uuid4
request_id = RequestIDMiddleware(request_id_factory=lambda: f'app-{uuid4()}')
The factory must be a synchronous callable that returns a string.
Providing an Existing Request ID¶
You can provide a request ID through request extensions using the request_id key.
The middleware uses the supplied ID instead of generating a new one.
The request ID is added using setdefault, so an existing request header is preserved. The response header is also only added when it does not already exist.
Request ID Extensions Type¶
The middleware also provides an Extensions type that extends the base OxideHTTP extensions schema with an optional request ID:
from oxidehttp.schema import Extensions as BaseExtensions
class Extensions(BaseExtensions):
request_id: str | None
This allows request IDs to be represented in the typed extensions schema.
Rate Limiting¶
RateLimitMiddleware limits how frequently requests are allowed to proceed.
It uses a RateLimitStore implementation to track acquisitions. OxideHTTP includes an in-memory store and a Redis-backed store.
Basic Usage¶
from oxidehttp.config import OxideHTTPConfig, RequestConfig
from oxidehttp.middlewares.ratelimit.middleware import RateLimitMiddleware
from oxidehttp.middlewares.ratelimit.stores.memory import MemoryRateLimitStore
rate_limit = RateLimitMiddleware(MemoryRateLimitStore(), limit=10, period=60.0)
config = OxideHTTPConfig(request=RequestConfig(middlewares=(rate_limit,)))
This configuration allows up to 10 acquisitions per 60-second window for each rate-limit key.
Options¶
| Argument | Type | Default | Description |
|---|---|---|---|
store |
RateLimitStore |
Required | Backend used to track rate limits. |
limit |
int |
Required | Maximum number of acquisitions per period. |
period |
float |
Required | Rate-limit window duration in seconds. |
Validation rules:
limitmust be at least1.periodmust be greater than0.
Rate-Limit Keys¶
The middleware constructs a key using the request hostname and an optional rateLimitKey request extension.
The resulting key follows this format:
If no extension key is provided, only the hostname is used.
For example, requests to:
share the same rate-limit bucket by default because they use the same hostname.
Custom Rate-Limit Keys¶
Use the rateLimitKey request extension to separate rate-limit buckets:
Another request can use a different bucket:
For the same hostname, these requests use different keys:
This is useful when different API endpoints have independent rate limits.
The extension key is optional. If omitted, requests to the same hostname share a bucket.
MemoryRateLimitStore¶
MemoryRateLimitStore stores rate-limit windows in process memory:
from oxidehttp.middlewares.ratelimit.stores.memory import MemoryRateLimitStore
store = MemoryRateLimitStore()
Characteristics:
- State is local to the current process.
- State is lost when the process restarts.
- Separate application processes do not share rate-limit state.
- Concurrent acquisitions are coordinated using an asynchronous lock.
- Requests wait asynchronously when the limit is reached.
Use this backend when rate limiting only needs to apply within a single process.
RedisRateLimitStore¶
RedisRateLimitStore stores rate-limit counters in Redis, allowing multiple application instances to share rate-limit state.
Install the Redis dependency:
Create the store using an asynchronous Redis client:
from redis.asyncio import Redis
from oxidehttp.middlewares.ratelimit.stores.redis import RedisRateLimitStore
redis = Redis.from_url('redis://localhost:6379')
store = RedisRateLimitStore(redis)
Options¶
| Argument | Type | Default | Description |
|---|---|---|---|
redis |
Redis |
Required | Asynchronous Redis client. |
prefix |
str |
"oxidehttp:ratelimit:" |
Prefix used for Redis keys. |
You can customize the prefix:
The Redis implementation uses a Lua script to atomically increment counters and initialize their expiration.
When the configured limit is exceeded, the store waits for the remaining key expiration time before attempting to acquire again.
Choosing a Store¶
| Store | Best suited for |
|---|---|
MemoryRateLimitStore |
Single-process applications. |
RedisRateLimitStore |
Multiple processes or application instances sharing a Redis server. |
Both stores implement the RateLimitStore protocol and can be used with RateLimitMiddleware.
RateLimitStore Protocol¶
Custom storage backends can implement the RateLimitStore protocol:
class RateLimitStore(Protocol):
async def acquire(
self, key: str, *, limit: int, period: float
) -> float: ...
The method receives the rate-limit key, maximum number of acquisitions, and window duration.
It returns the amount of time waited, in seconds. RateLimitMiddleware awaits this method before forwarding the request.
Rate-Limit Extensions¶
The middleware provides an Extensions type for its request extension:
Use rateLimitKey when you need independent rate-limit buckets for requests sharing the same hostname.