========== Middleware ========== Middleware ========== Middlewares are functions that wrap your core request handler, forming a chain. Each middleware can inspect or modify the ``Request`` before it is sent, and the ``Response`` before it is returned. .. code-block:: text client.get(url) │ ▼ ┌─────────────────┐ │ logging │ ← outermost: sees every attempt │ ┌────────────┐ │ │ │ retry │ │ ← wraps the actual dispatch │ │ ┌───────┐ │ │ │ │ │adapter│ │ │ ← innermost: performs the HTTP request │ │ └───────┘ │ │ │ └────────────┘ │ └─────────────────┘ │ ▼ Response Middlewares are executed in the order they are passed to ``HttpClient``. The first item in the list is the outermost wrapper; the last item is closest to the adapter. ---- Built-in Middlewares -------------------- retry ~~~~~ Automatically retries requests that fail with specified status codes or connection errors. .. code-block:: python from src.middleware import retry middleware=[retry(retries=3, delay=0.1)] **Parameters:** - ``retries`` *(int)* — Number of retry attempts. Defaults to ``3``. - ``delay`` *(float)* — Delay in seconds between retries. Defaults to ``0.1``. - ``retry_on_status_code`` *(set)* — HTTP status codes to retry on. Defaults to ``{500, 502, 503, 504}``. .. note:: ``429 Too Many Requests`` is **not** included in the default retry set. This is intentional — retrying a rate-limited request immediately will likely result in another ``429``. If you want to handle it, pass it explicitly and combine with a longer ``delay``: .. code-block:: python retry(retries=3, delay=2.0, retry_on_status_code={429, 500, 502, 503, 504}) logging ~~~~~~~ Logs the HTTP method, URL, response status code, and elapsed time for each request. .. code-block:: python from src.middleware import logging middleware=[logging()] **Parameters:** - ``logger`` *(callable)* — Function used for logging. Defaults to ``print``. By default this uses ``print``, which is convenient for development. For production, plug in Python's standard ``logging`` module instead: .. code-block:: python import logging as stdlib_logging from src.middleware import logging as log_middleware logger = stdlib_logging.getLogger(__name__) middleware=[log_middleware(logger=logger.info)] timeout ~~~~~~~ Enforces a maximum duration for the entire request processing. Works with any adapter. .. code-block:: python from src.middleware import timeout middleware=[timeout(seconds=10.0)] **Parameters:** - ``seconds`` *(float)* — Timeout duration in seconds. Defaults to ``10.0``. **Raises:** ``TimeoutError`` if the request does not complete within the specified duration. .. warning:: ``lightreq`` automatically suppresses the adapter's own timeout when this middleware is present, to prevent a double-timer conflict. For example, when using the ``requests`` adapter, its built-in ``timeout`` parameter is disabled in favour of this middleware's thread-based timer. Do not set both simultaneously. rate_limit ~~~~~~~~~~ Enforces a maximum number of requests within a rolling time window. Uses a thread-safe sliding-window counter — requests are never silently dropped, but delayed until they can proceed. .. code-block:: python from src.middleware import rate_limit middleware=[rate_limit(calls=10, period=1.0)] **Parameters:** - ``calls`` *(int)* — Maximum number of calls allowed within ``period``. Defaults to ``10``. - ``period`` *(float)* — Time window in seconds. Defaults to ``1.0``. **Raises:** ``ValueError`` if ``calls`` is less than 1, or ``period`` is not positive. .. note:: ``rate_limit`` is thread-safe. A single client instance can be safely shared across threads and the sliding window will be shared consistently across all of them. ---- Ordering Guide -------------- The order in which middlewares are defined has significant effects on behavior. Below are the most important cases to understand. ``logging`` + ``retry`` ~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python # logging wraps retry — only logs once per client.get() call, # regardless of how many retries happen internally middleware=[logging(), retry(retries=3)] # retry wraps logging — logs every individual attempt, including retries middleware=[retry(retries=3), logging()] Place ``retry`` inside ``logging`` (i.e. ``logging`` first) if you only want one log line per logical request. Place ``logging`` inside ``retry`` (i.e. ``retry`` first) if you want to trace every retry attempt individually. ``timeout`` + ``retry`` ~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python # timeout wraps retry — all retry attempts share a single timer. # If the total time across retries exceeds the timeout, a TimeoutError is raised. middleware=[timeout(seconds=5.0), retry(retries=3)] # retry wraps timeout — each attempt gets its own independent timer. # A single slow attempt won't abort the remaining retries. middleware=[retry(retries=3), timeout(seconds=5.0)] In most cases, placing ``retry`` before ``timeout`` (each attempt gets its own timer) is the safer choice. ``retry`` + ``rate_limit`` ~~~~~~~~~~~~~~~~~~~~~~~~~~ .. warning:: Retried requests count against the rate limit. If you have a strict rate limit and a high retry count, retries may be delayed significantly by the rate limiter. Design your ``calls`` and ``period`` values accordingly. ---- Middleware Cookbook ------------------- The following are ready-to-use combinations for common scenarios. Resilient production client ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Retries transient server errors, enforces a per-attempt timeout, and rate-limits outgoing traffic. Logging uses the standard library for structured output. .. code-block:: python import logging as stdlib_logging from src.client import HttpClient from src.middleware import logging as log_middleware, retry, timeout, rate_limit logger = stdlib_logging.getLogger(__name__) client = HttpClient( adapter="requests", middleware=[ log_middleware(logger=logger.info), rate_limit(calls=10, period=1.0), retry(retries=3, delay=0.5), timeout(seconds=5.0), ] ) Verbose development client ~~~~~~~~~~~~~~~~~~~~~~~~~~ Logs every retry attempt individually. Useful when debugging flaky endpoints. .. code-block:: python from src.client import HttpClient from src.middleware import logging, retry, timeout client = HttpClient( adapter="urllib", middleware=[ retry(retries=3, delay=0.2), logging(), timeout(seconds=10.0), ] ) ---- Writing Custom Middleware ------------------------- Custom middlewares are closures that accept a ``next_handler`` and return a ``handler`` function. .. code-block:: python def my_custom_middleware(next_handler): def handler(request): # modify or inspect the request before sending print("Sending request to:", request.url) response = next_handler(request) # modify or inspect the response before returning print("Received status:", response.status_code) return response return handler Pass it to ``HttpClient`` like any built-in middleware: .. code-block:: python from src.client import HttpClient client = HttpClient( adapter="requests", middleware=[my_custom_middleware] )