BaseHTTPMiddleware as Anti-Pattern

BaseHTTPMiddleware as Anti-Pattern

This week I was working on something related to BaseHTTPMiddleware where, due to StreamingResponse and specific production settings, it can transform your async REST API into a completely synchronous one.

The issue arises because BaseHTTPMiddleware can cause hanging when used with StreamingResponse. As mentioned in some discussions, the reading of the response body can get exhausted in the first read, and when it comes to the second read, it waits indefinitely, leading to a blocked application. In ASGI applications, messages are sent between the client and the app using types like http.response.start and http.response.body.

To avoid this, you can use pure ASGI middleware instead of BaseHTTPMiddleware:

from starlette.types import ASGIApp, Receive, Send, Message, Scope

class LogResponseMDW:

    def __init__(self, app: ASGIApp) -> None:
        self.app = app
    
    async def __call__(self, scope: Scope, receive: Receive, send: Send):
        if scope["type"] != "http":
            await self.app(scope, receive, send)
            return

        async def send_wrapper(message: Message):
            # This will capture response coming from APP Layer
            # You will want to check for message["type"] first
            # response body will be in message where the type is
            # "http.response.body"
            if message["type"] == "http.response.body":
                print(f"message: {message}") # log here
            await send(message)
        
        await self.app(scope, receive, send_wrapper)

# You can add this to your app this way:
# app.add_middleware(LogResponseMDW)

For more context, see this StackOverflow discussion.