-
-
Notifications
You must be signed in to change notification settings - Fork 39
Description
- Explain the use-case carefully. Maybe with a code example
The current implementation of hooks in the requests module and niquests uses a Dict[str, Callable] to manage hooks tied to specific events in the request lifecycle (e.g., request, response). While functional, this approach limits flexibility because it ties hooks to predefined event names and doesn’t easily allow for ordered execution, conditional skipping, or more complex logic. I propose replacing this with a list of hook objects based on an abstract base class, RequestHook, which provides methods for handling different stages of the request lifecycle (on_request, on_response, on_exception). Each method returns a boolean to indicate whether further hooks should be skipped, offering fine-grained control.
Here’s an example implementation:
import niquests
import logging
from abc import ABC
from niquests import PreparedRequest, Response, Request
from niquests import exceptions
class RequestHook(ABC):
async def on_request(self, request: PreparedRequest) -> bool:
"""
Process the request before it is sent to the server.
Args:
request: The HTTP request configuration to process.
Returns:
True if request processing is complete and further hooks should be skipped, False otherwise.
"""
pass
async def on_response(self, response: Response) -> bool:
"""
Process the response after it is received from the server.
Args:
response: The HTTP response received from the server.
Returns:
True if response processing is complete and further hooks should be skipped, False otherwise.
"""
pass
async def on_exception(self, exception: exceptions.RequestException) -> bool:
"""
Handle an exception raised during request life cycle.
Args:
exception: The exception that was raised. (contains request and response if received)
Returns:
True if the exception was handled, False if it should be propagated.
"""
pass
class AuthHook(RequestHook):
def __init__(self, bearer_token: str) -> None:
self.__bearer_token: str = bearer_token
async def on_request(self, request: PreparedRequest) -> bool:
request.headers["Authorization"] = f"Bearer {self.__bearer_token}"
return False
class LoggingHook(RequestHook):
def __init__(self, logger: logging.Logger) -> None:
self.__logger: logging.Logger = logger
async def on_request(self, request: PreparedRequest) -> bool:
self.__logger.debug(f"Request: {request.method} {request.url}")
return False
async def on_response(self, response: Response) -> bool:
self.__logger.debug(f"Response: {response.status_code} {response.text}")
return False
session = niquests.Session()
session.hooks = [LoggingHook(logging.getLogger())]
auth_request = Request(method="GET", url="https://api.example.com/auth")
prepped_auth_request = session.prepare_request(auth_request)
prepped_auth_request.hooks.append(AuthHook("your_bearer_token"))-
Who needs this?
This enhancement would benefit a wide range of developers using niquests or requests, particularly:Web developers building APIs or clients who need precise control over request/response flows (e.g., adding authentication, retry logic, or custom logging).
Middleware authors who want to create reusable, composable components for HTTP workflows.
Enterprise developers managing complex request pipelines where conditional logic or early termination is critical (e.g., security checks, rate-limiting enforcement).
Anyone needing more than simple callback-based hooks would find this valuable. -
What pain does this resolve?
The current Dict[str, Callable] approach has several limitations:Lack of ordering: Hooks are tied to event names, not an explicit sequence, making it hard to enforce execution order.
No conditional control: There’s no built-in way to skip remaining hooks based on a condition (e.g., stopping after an authentication failure).
Limited extensibility: Adding new lifecycle events requires modifying the core library, and complex logic is harder to encapsulate in a single callable. -
Is this standard?
This approach isn’t a universal standard in Python HTTP libraries but aligns with patterns in other frameworks and ecosystems:Middleware patterns: Popular in web frameworks like Flask, Django, and FastAPI, where middleware executes in a defined order and can short-circuit the pipeline (e.g., returning a response early).
Hook systems in other languages: For example, Node.js’ Express middleware and Ruby’s Rack use ordered, skippable handlers.
While not identical to requests’ current API, this proposal modernizes it in a way that’s intuitive for developers familiar with middleware or interceptor patterns. It’s a natural evolution for niquests.