Skip to content

Middleware Pipeline Overview

WD-DI includes a flexible middleware pipeline that allows you to compose processing logic in a sequence. This pattern is particularly well-suited for handling cross-cutting concerns in your application—functionality that applies to many parts of your system but isn't part of the core business logic of any single component.

Think of a middleware pipeline as a series of processing steps that a request (or a context object representing that request) goes through. Each middleware component in the pipeline has the opportunity to:

  • Inspect the request/context.
  • Modify the request/context.
  • Perform actions before or after the next middleware in the pipeline is called.
  • Short-circuit the pipeline and return a response immediately.

Common Use Cases for Middleware

  • Logging: Recording details about incoming requests and outgoing responses.
  • Authentication/Authorization: Verifying credentials and checking permissions.
  • Error Handling: Catching exceptions from later middleware or handlers and formatting appropriate error responses.
  • Caching: Serving cached responses for certain requests to improve performance.
  • Request/Response Manipulation: Modifying headers, transforming data formats.
  • Validation: Validating incoming data before it reaches core business logic.

Defining Middleware (IMiddleware)

To create a middleware component, you implement the IMiddleware interface (or more accurately, a class with an async def invoke(self, context, next) method, as Python uses structural subtyping here).

  • context: An object representing the current request or operation. You define the structure of this context object based on your application's needs.
  • next: An awaitable callable that invokes the next middleware in the pipeline. You must await next() to pass control to the subsequent middleware. If you don't call it, the pipeline short-circuits at your middleware.

Example of a custom middleware:

from wd.di.middleware import IMiddleware # IMiddleware is a type hint helper

class CustomAuthMiddleware: # Implements IMiddleware structurally
    async def invoke(self, context, next_middleware):
        # Example: Assume context has an 'is_authenticated' attribute
        if not getattr(context, 'is_authenticated', False):
            # You might raise an error or set a response indicating unauthorized
            raise PermissionError("User not authenticated.")

        print("User is authenticated, proceeding...")

        # Call the next middleware in the pipeline
        response = await next_middleware()

        # You can also process the response after it comes back up the chain
        print("CustomAuthMiddleware finished.")
        return response

Configuring the Pipeline

WD-DI uses a MiddlewareBuilder (obtained via services.create_application_builder().configure_middleware(...)) to configure the pipeline. You chain use_middleware calls to add middleware components in the desired order of execution.

Example:

from wd.di import create_service_collection
from wd.di.middleware import LoggingMiddleware # An example built-in
# from .custom_middleware import CustomAuthMiddleware # Assuming CustomAuthMiddleware is defined

# For demonstration, let's define CustomAuthMiddleware and a dummy context here
class CustomAuthMiddleware:
    async def invoke(self, context, next_middleware):
        if not getattr(context, 'is_authenticated', False):
            raise PermissionError("User not authenticated.")
        print(f"Auth: User authenticated for path {context.path}")
        response = await next_middleware()
        return response

class DummyFinalHandler: # Represents the end of your main processing logic
    async def invoke(self, context, next_middleware): # next_middleware won't be called here
        print(f"Handler: Processing context for path {context.path}")
        return f"Successfully processed {context.path}"

class RequestContext:
    def __init__(self, path: str, is_authenticated: bool = False):
        self.path = path
        self.is_authenticated = is_authenticated

sc = create_service_collection()

# Create an application builder from the service collection
app_builder = sc.create_application_builder()

# Configure the middleware pipeline
app_builder.configure_middleware(lambda builder: (
    builder
    .use_middleware(LoggingMiddleware)  # Built-in, assuming it's registered or takes a logger
    .use_middleware(CustomAuthMiddleware)
    .use_middleware(DummyFinalHandler) # Your actual request handler might be the last "middleware"
))

# Build the service provider
provider = app_builder.build()

# Get the configured middleware pipeline
# The pipeline itself is registered as a service
from wd.di.middleware import MiddlewarePipeline 
pipeline = provider.get_service(MiddlewarePipeline)

# --- Execute the pipeline ---
# import asyncio # Required if running top-level await

async def main():
    # Example 1: Authenticated request
    print("--- Running Authenticated Request ---")
    auth_context = RequestContext(path="/secret-data", is_authenticated=True)
    try:
        result_auth = await pipeline.execute(auth_context)
        print(f"Pipeline Result (Authenticated): {result_auth}")
    except Exception as e:
        print(f"Error (Authenticated): {e}")

    print("\\n--- Running Unauthenticated Request ---")
    # Example 2: Unauthenticated request
    unauth_context = RequestContext(path="/secret-data", is_authenticated=False)
    try:
        result_unauth = await pipeline.execute(unauth_context)
        print(f"Pipeline Result (Unauthenticated): {result_unauth}")
    except Exception as e:
        print(f"Error (Unauthenticated): {e}")

# To run this example:
# asyncio.run(main())

When pipeline.execute(context) is called, the context object flows through LoggingMiddleware, then CustomAuthMiddleware, and finally DummyFinalHandler (if authentication passes). The order of registration with use_middleware matters.


Middleware Dependencies

Middleware components themselves can have dependencies, which will be resolved by the DI container when the middleware is created for the pipeline. This allows your middleware to use other services (e.g., a logging service, a configuration service).

The MiddlewareBuilder internally uses the ServiceCollection it was created with to build a temporary ServiceProvider to resolve middleware instances each time pipeline.execute() is called. This ensures middleware dependencies are resolved with the correct lifetimes (e.g., scoped dependencies for a scoped pipeline execution).


By leveraging the middleware pipeline, you can create clean, modular, and reusable components for handling tasks that span across multiple parts of your application.