Skip to content

Bug Report: SlowAPIASGIMiddleware breaks StreamingResponse by sending http.response.start multiple times #249

@vkuzmov

Description

@vkuzmov

Description

SlowAPIASGIMiddleware sends the http.response.start message on every http.response.body message, which violates the ASGI specification and causes streaming responses to fail with a LocalProtocolError: Too little data for declared Content-Length error.

Environment

  • slowapi version: 0.1.9
  • Python version: 3.13.2
  • FastAPI version: 0.116.1
  • OS: macOS (but likely affects all platforms)

Steps to Reproduce

  1. Create a FastAPI endpoint that returns a StreamingResponse with a Content-Length header:
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from slowapi import Limiter
from slowapi.middleware import SlowAPIASGIMiddleware

app = FastAPI()
app.state.limiter = Limiter(key_func=lambda: "test", default_limits=["10/minute"])
app.add_middleware(SlowAPIASGIMiddleware)

@app.get("/download")
async def download():
    async def stream_body():
        # Simulate streaming multiple chunks
        for i in range(10):
            yield f"chunk {i}\n".encode()

    headers = {
        "Content-Type": "application/octet-stream",
        "Content-Length": "100",  # Total size of all chunks
    }

    return StreamingResponse(stream_body(), headers=headers)
  1. Make a request to the endpoint: GET /download
  2. Observe the error

Expected Behavior

The streaming response should work correctly, sending:

  • One http.response.start message (with headers)
  • Multiple http.response.body messages (one per chunk)

Actual Behavior

The middleware sends:

  • Multiple http.response.start messages (one before each http.response.body)
  • Multiple http.response.body messages

This results in the error:

h11._util.LocalProtocolError: Too little data for declared Content-Length
elif message["type"] == "http.response.body":
    # ... header injection logic ...

    # send the http.response.start message just before the http.response.body one,
    # now that the headers are updated
    await self.send(self.initial_message)  # ❌ Sent on EVERY body message
    await self.send(message)

According to the ASGI specification, http.response.start must be sent exactly once per response, before any http.response.body messages.

Proposed Fix

Track whether the initial message has been sent and only send it once:

class _ASGIMiddlewareResponder:
    def __init__(self, app: ASGIApp) -> None:
        self.app = app
        self.error_response: Optional[Response] = None
        self.initial_message: Message = {}
        self.inject_headers = False
        self.initial_message_sent = False  # ✅ Add flag to track if sent

    async def send_wrapper(self, message: Message) -> None:
        if message["type"] == "http.response.start":
            self.initial_message = message
            self.initial_message_sent = False  # Reset flag

        elif message["type"] == "http.response.body":
            if self.error_response:
                self.initial_message["status"] = self.error_response.status_code

            if self.inject_headers:
                headers = MutableHeaders(raw=self.initial_message["headers"])
                headers = self.limiter._inject_asgi_headers(
                    headers, self.request.state.view_rate_limit
                )

            # ✅ Only send the http.response.start message once, before the first body message
            if not self.initial_message_sent:
                await self.send(self.initial_message)
                self.initial_message_sent = True

            await self.send(message)

Workaround

We've implemented a fixed version locally (FixedSlowAPIASGIMiddleware) that includes this fix. However, it would be better to have this fixed upstream.

Additional Context

  • This bug affects any endpoint using StreamingResponse with SlowAPIASGIMiddleware
  • The bug occurs regardless of whether rate limit headers are being injected
  • Non-streaming responses work fine because they only send one http.response.body message

References


Note: I'm happy to submit a pull request with this fix if the maintainers approve of the approach.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions