-
-
Notifications
You must be signed in to change notification settings - Fork 949
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Consume request body in middleware is problematic #495
Comments
Where is it blocking? Is there a second request instance involved? (otherwise it should just return, shouldn't it?) starlette/starlette/requests.py Lines 178 to 193 in 7eb4375
|
Looks like it: starlette/starlette/middleware/base.py Line 26 in 7eb4375
starlette/starlette/routing.py Line 38 in 7eb4375
Can you write a test for starlette itself? (just for reference: #498) |
"Consume request body in middleware is problematic" Indeed. Consuming request data in middleware is problematic. On the whole you should avoid doing so if at all possible. There's some work we could do to make it work better, but there will always be cases where it can't work (eg. if you stream the request data, then it's just not going to be available anymore later down the line). |
Coming from FastAPI issue referenced above. @tomchristie I don't understand the issue about consuming the request in a middleware, could you explain this point ? In fact, I have the need (which is current where I work) to log every requests received by my production server. Is there a better place than a middleware to do it and avoid duplicating code in every endpoint ? (I would like something like this https://github.com/Rhumbix/django-request-logging) For now, I found this workaround (but that's not very pretty):
I kind of disagree with your example. In fact, stream data is not stored by default, but stream metadata (is the stream closed) are; there will be an understandable error raised if someone try to stream twice, and that is enough imho. That's why if the body is cached, the stream consumption has to be cached too. |
There are plenty use cases like @wyfo mention. In my case i'm using JWT Signature to check all the data integrity, so i decode the token and then compare the decoded result with the body + query + path params of the request. I don't know any better way of doing this |
I'm working on a WTForms plugin for Starlette and I'm running into a similar issue. What is the recommended way to consume |
Currently, this is one of the better options: https://fastapi.tiangolo.com/advanced/custom-request-and-route/#accessing-the-request-body-in-an-exception-handler but unfortunately there still isn't a great way to do this as far as I'm aware. |
Thanks! Is there an equivalent to |
@amorey Depending on exactly what you want to do, you can either create a custom middleware that only does things before the request, or you can create a dependency that does whatever setup you need, and include it in the router or endpoint decorator if you don't need access to its return value and don't want to have an unused injected argument. I think that's the closest you'll get to a direct translation of the |
@dmontagu I am using FastAPI and trying to log all the 500 Internal Server error only by using the ServerErrorMiddleware from Starlette using add_middleware in FastAPI. Conversation from Gitter but lmk if anybody have feedbacks async def exception_handler(request, exc) -> Response:
print(exc)
return PlainTextResponse('Yes! it works!')
app.add_middleware( ServerErrorMiddleware, handler=exception_handler ) Okay I found a way to use with starlette-context to retrieve my payload only when I need logging I am writing every request using an incoming middleware with context in starlette-context and retrieving through context.data at HTTPException and ServerErrorMiddleware. This is not required in ValidationError as FastAPI support natively by using exc.body. All this roundabout only when you need to log those failed responses. You dont need an middleware for logging incoming requests if you dont want to simply use depends in your router application.include_router(api_router, prefix=API_V1_STR, dependencies=[Depends(log_json)]) |
is there an update for this? is there a plan to allow consuming the body in a middleware ? any other solution would also be ok, but i do feel this i needed @JHBalaji how are you logging the request body to context, are you not running into the same issue when trying to access the body in the incoming request middleware? |
Another use case for this: decompressing a POST request's body. Example (which does not work, but would be amazing if it did): @app.middleware("http")
async def decompress_if_gzip(request: Request, call_next):
if request.headers.get("content-encoding", "") == "gzip":
body = await request.body()
dec = gzip.decompress(body)
request._body = dec
response = await call_next(request)
return response Any better place to do this than in the middleware? |
@talarari
You would add the middleware in the app then you could access as context.data @JivanRoquet
|
@JHBalaji unless I'm mistaken, this GZipMiddleware is to gzip the response's body — not unzipping the request's body |
@JivanRoquet yes but the FastAPI documentation does have what you might be looking for. |
@JHBalaji yes I've seen that since then, thank you for following up. |
@kigawas I am using this in production with multiple middlewares before and after this one. Perhaps you are doing something that I am not and hence hitting this problem Edit: trying to reuse this solution later in another project causes problems. Could be FastAPI version dependent. |
any updates? |
I think we can do it in this way:
|
Does |
Same issue, yes. |
Thanks for the confirmation, I ended up adapting an idea from FastAPI. Essentially my whole app runs as one middleware, and I pulled the same the handler lookup from |
uvicorn ->app(receive,send) -> receive = queue,get() if log message by receive,receive will be consume and request will block because nothing in queue,get() |
You mean this? This definitely works and it's pretty smart 😄 diff --git a/starlette/requests.py b/starlette/requests.py
index 66c510c..69cad4c 100644
--- a/starlette/requests.py
+++ b/starlette/requests.py
@@ -188,11 +188,22 @@ async def empty_send(message: Message) -> typing.NoReturn:
class Request(HTTPConnection):
+ def __new__(
+ cls, scope: Scope, receive: Receive = empty_receive, send: Send = empty_send
+ ):
+ if "request" in scope:
+ return scope["request"]
+
+ obj = object.__new__(cls)
+ scope["request"] = obj
+ return obj |
@kigawas so this modification didnt merge into master, right? |
@tomchristie we're currently using the above workaround using the code below:
this is working great for us. What would you think about maybe adding some kind of mixin class to starlette? maybe like a |
This is very similar to our workaround.
|
I'm posting in this 4-year old issue this with the hope that someone will see it and it will lead to a better understanding of the problem and ultimately a fix, because I have run into this problem many times, and because it doesn't throw an error, it just hangs the program it always takes me too long to figure out what's going on. I think there is some confusion about what the problem is. I don't believe people are confused about how to wrap If I only wrap the The problem comes when you try to consume the request body (i.e., call async def __call__(self, scope, receive, send):
body = b""
while True:
message = await receive()
chunk: bytes = message.get("body", b"")
body += chunk
if message.get("more_body", False) is False:
break
For me, all that's needed is a tiny, undocumented property on async def __call__(self, scope, receive, send):
body = b""
# ... consume the body
scope["state"]["previously_consumed_body"] = body
self.app(scope, receive, send) And in async def body(self) -> bytes:
if (previously_consumed_body := self.scope["state"].get("previously_consumed_body")):
return previously_consumed_body
# else, the rest of the function is the same Sure, it's hacky. But it's just one line of code, doesn't need to be documented (i.e. comes with the understanding that using it is risky), adds virtually no request overhead, but will save me and probably thousands of other developers from having to refactor their middleware stack or routes, fork this repository, or use another library. If someone needs a fix now, you can do something like this to trick Starlette (this is similar to other workarounds already mentioned above). Note that this code isn't production-ready: async def __call__(self, scope, receive, send):
body = b""
# ... consume the body
async def dummy_receive():
return {
"type": "http.request",
"body": body,
"more_body": False,
}
await self.app(scope, dummy_receive, send) |
This issue seems to be stuck on a new PR: #1519 |
You can always insert an empty body message:
Or something like that |
The text was updated successfully, but these errors were encountered: