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
- 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)
- Make a request to the endpoint:
GET /download
- 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.
Description
SlowAPIASGIMiddlewaresends thehttp.response.startmessage on everyhttp.response.bodymessage, which violates the ASGI specification and causes streaming responses to fail with aLocalProtocolError: Too little data for declared Content-Lengtherror.Environment
Steps to Reproduce
StreamingResponsewith aContent-Lengthheader:GET /downloadExpected Behavior
The streaming response should work correctly, sending:
http.response.startmessage (with headers)http.response.bodymessages (one per chunk)Actual Behavior
The middleware sends:
http.response.startmessages (one before eachhttp.response.body)http.response.bodymessagesThis results in the error:
h11._util.LocalProtocolError: Too little data for declared Content-LengthAccording to the ASGI specification,
http.response.startmust be sent exactly once per response, before anyhttp.response.bodymessages.Proposed Fix
Track whether the initial message has been sent and only send it once:
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
StreamingResponsewithSlowAPIASGIMiddlewarehttp.response.bodymessageReferences
Note: I'm happy to submit a pull request with this fix if the maintainers approve of the approach.