Repository: encode/starlette

Issue:
Fixes Request.json() Hangs Forever
This fixes #847 by using the Request scope to store consumed stream data so that new instances of Request can access the Request stream content. Previously `await Request(...).json()` followed by another `await Request(...).json()` would hang forever because the `receive` stream was exhausted and `await receive()` doesn't timeout after message `{"more_body": False}` is received. This PR is focused only on enabling `Request.body()`, `Request.form()` and `Request.json()` to return the cached data as originally intended and does not address the issue of `await receive()` hanging forever inside of `Request.stream()`
Consume request body in middleware is problematic
```
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import PlainTextResponse
from starlette.middleware.base import BaseHTTPMiddleware


class SampleMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        _ = await request.form()
        return await call_next(request)


app = Starlette()


@app.route('/test', methods=['POST'])
async def test(request):
    _ = await request.form()  # blocked, because middleware already consumed request body
    return PlainTextResponse('Hello, world!')


app.add_middleware(SampleMiddleware)
```

```
$ uvicorn test:app --reload
```

```
$ curl -d "a=1" http://127.0.0.1:8000/test
# request is blocked
```


Discussion:
is there a reason nobody had a look at this yet? seems fairly straight forward and fixes an issue that we keep stumbling over.
Any love for this issue?  It's been quiet in here for months.
is this fixed by the anyio port?
Any updates?
Would be really nice to see this go in so multiple calls to `await request.form()` etc. are managed automatically--- I'm currently storing the result in `request.state` in the interim so I can make use of it in middlewares, as well as endpoints.
Possible to give this a closer look? @tomchristie  This would close the following tickets: 

* https://github.com/encode/starlette/issues/495
* https://github.com/encode/starlette/issues/771
* https://github.com/encode/starlette/issues/847
* https://github.com/encode/starlette/issues/493
This PR is not compliant with the [ASGI HTTP Connection Scope](https://asgi.readthedocs.io/en/latest/specs/www.html#http-connection-scope). It doesn't feel right to modify the scope dictionary.

EDIT: My comment is actually invalidated by the one below :)
If i'm not mistaken `state` is not compliant either, yet Starlette defines it in scope and it's extremely useful. 

I highly appreciate the team being extra critical of additions to keep Starlette minimalist, but there are going to be more and more issues filed about this as Starlette increases in popularity. It's far too common of a roadbump people are actively running into.
Are there any progress or discussions going on around this?
@nikordaris Are you still interested in helping with this PR?
I left a comment on Tom's comment. From my perspective I'm not seeing anything else for me to change with this PR. Let me know if there is anything simple I can tweak on this PR to help it get merged. As I'm no longer on this tech stack I won't likely have time to do any major modifications to this PR
Ok, i just read through all the comments above and am slowly remembering all the context around this as i've not looked into this in over a year. I can see there is an issue with following the ASGI spec for `scope` but starlette is already breaking that with the use of `state`. I _think_ the correct solution here is to use scope [extensions](https://asgi.readthedocs.io/en/latest/specs/main.html#extensions) to store this stuff (technically i think state should probably also be in there). i can update this PR to move the body data to `scope['extensions']['request_cache']['body']` which i think will resolve the concerns around this. i can go ahead and just store the `body` and `form` and take the json load hit as per @tomchristie's comment. i think the question is what should the namespace be under `extensions`? should it be `starlette`? or more generic `request_cache`? i'm leaning towards the generic `request_cache`, but i also don't really care since i don't really have any skin in this anymore. whenever y'all decide on this `@me` so it'll show up in my github notifications.
+1 for request_cache
No newcomer is going to want to hunt down `request_cache` just so their request wont hang.

@nikordaris's original PR works how newcomers will expect this to work out of the box- You get the correct behaviour without having to do anything.


@gnat I'm not sure I'm following your concern. I would expect the fact it is stored in `request_cache` within extensions would be transparent to new comers. They would just use `Request.json` to access the json data. If extensions.request_cache had a body it would use that if not it would pull it from the stream. I wouldn't expect users of Starlette to know anything about the request_cache extension.
Thank you for the clarification @nikordaris That sounds totally fine.
Ok, i got nerd sniped on this. I moved all the caching stuff to the `Request.stream()`. This will now cache the raw bytes stream instead of caching it after parsing it for either body, json, or form. This removes the need to track and raise a stream consumed runtime error. I also moved the cache store to property methods so that it can easily be updated to point to wherever y'all want it to. Right now i'm pointing it to `scope["extensions"]["starlette"]["stream"]`. I went with `starlette` as the namespace here because of other framework conventions where the namespace is the owner of the data. In this case, the owner is `starlette`. Whereas a third-party library that worked on-top of starlette would reserve their own extensions space under their name to signify their ownership and control of the data. With that said, if y'all want to name it to something else or just throw it directly on the scope, i can update this PR to point it to the preferred location. just `@me` in case i miss any responses to this PR. From my perspective, these changes address all the issues raised in the comments above and is ready to merge as long as there are no objections to the location within `scope` for the cached bytes stream. @tomchristie 
Caching the stream is not what we want to do.

In fact we particularly *don't* want to do that, because if we're *streaming* the request we're deliberately ingesting it in a way that doesn't store the entire stream in memory at any one time.

I'd really like to see any changes here string from a POV of narrowing down the exact behaviour that's problematic to a simple-as-possible example case, so that we can properly talk through what's actually needed.
> I'd really like to see any changes here string from a POV of narrowing down the exact behaviour that's problematic to a simple-as-possible example case, so that we can properly talk through what's actually needed.

My use case was handling of uncaught exceptions before sending the data to Sentry I wanted to get access to certain parameters that were part of a POST request to pass such data in a more structured way to Sentry.
@tomchristie it is already caching in the stream but only specifically when the `Request.body` caches it. This creates a non-intuitive experience for the user. I'm struggling to know what you want here. Your previous comment wanted to consolidate the json, body and form into a single cache variable. Doing it at the raw stream is the only way without major refactors with the Form parsers. What I can do is move the stream caching out of `Request.stream` and into a private function that wraps the stream and then have `Request.body` and `Request.form` call it instead of the stream. This would leave the public stream function as a pure Generator. I do think the current caching for the body needs to be removed though if we want to make it a pure Generator.
>  it is already caching in the stream but only specifically when the Request.body caches it

Exactly, yes. If you access `request.body` then you're working with the entire payload in memory. At that point you may as well cache it. You've accessed it, it's in memory, and you might as well keep it there for the duration of the request/response.

> I'm struggling to know what you want here.

What I'd like is for the issue to be well described, with a simple-as-possible "this behaviour isn't what I want, when I do *this*", so we can work through to a better resolution than caching the stream.

Having said that I've taken another look at #847 and that's pretty much the sort of thing I'm asking for.

The resolution to that (it seems to me) would be to cache the body. (Rather than the stream.)

Ok. Where did you want the cache stored in the scope? In extensions? Directly on the scope? If extensions what namespace? starlette? request_cache? @tomchristie 

I'll do one more pass at this and then I'm done. I'm not on this tech stack anymore and cant invest any more time on this. Someone else will have to fork my PR and resubmit it with any follow-up changes.

My approach will be to cache the raw stream only for body and form. If stream is used directly it will use the cache if it exists. If the stream is consumed and there is no cache then it will raise a runtime error.
Ok, @tomchristie this is ready for review. The `Request.stream` no longer caches. If `Request.body` or `Request.form` is called it will cache the raw bytes stream into `scope.extensions.starlette.stream`. If `Request.stream` is called followed by `Request.body`, a runtime error will be thrown indicating the stream has been consumed. If `Request.body` or `Request.form` is called followed by `Request.stream`, no error is raised and the cached stream will be returned. I think this lines up with your concerns around a large stream getting stored in memory while still preserving the convenience of leveraging the cache from body or form. Once you decide where in the `scope` you want to store the cached stream, i'll update that location and then this PR should be complete. 
Just bumping by to mention that this *is* on my personal backlog at the moment, but I don't want to deal with it until I can give it some proper attention, since it's a bit of a fiddly area.

Please do feel free to nudge me next week if I don't already get onto looking at it before then.
Okay, so I'd been pushing back against this approach because it changes the nature of streaming the request.

If we cache all the data we get when we stream a request, then we're always going to store the entire body in memory. That's find in cases where you're accessing the body (because you needed it all in memory in that cases), or in cases where you're accessing the request as JSON (because, same). Tho it's not ideal in cases where you're accessing `request.form()`, because in that case we're careful that the request parsing deliberately *doesn't* store the entire body in memory.

There's an alternate approach that'd essentially be to raise an error there's an attempt to stream the request more than once but that *does* cache the result of `.body` and `.form`.

However at this point, I'm kinda thinking that we should possibly just suck it up, and accept the trade-off because it results in a much simpler set of "here's what you can and can't do when repeatedly accessing the request content".

Anyone?
> If we cache all the data we get when we stream a request, then we're always going to store the entire body in memory. That's fine in cases where you're accessing the body (because you needed it all in memory in that cases), or in cases where you're accessing the request as JSON (because, same). Tho it's not ideal in cases where you're accessing `request.form()`, because in that case we're careful that the request parsing deliberately _doesn't_ store the entire body in memory.

I think I agree with what you say, but I'm not understanding the relationship to this PR.
Let me echo back to you my interpretation to see if I'm missing anything.

My understanding is that currently in Starlette if you do `Request.body()` (or anything that calls that, like `Request.json()`) the body gets cached. On the other hand, if you do `Request.stream()` (or anything that calls that, like `Request.form()`) then the body is _not_ cached in memory. From my understanding of this PR that is preserved, the only difference is that when the body _is_ cached it is cached into the ASGI scope instead of the Request instance. Right @nikordaris ?

More generally, I think it would make sense to have an API like `Request.form(consume=False)` and `Request.json(consume=True)` (with defaults to the current behavior) that allow users to control this. That would I think aleviate confusion because instead of having to dig into the implementation details of `Request.json` or `Request.form` (or look at some footnote in the docs) you can see just from the function signature what's going on. Not only that, but if you're looking for this info it's probably because you want the opposite behavior, so you also already know how to change it. But again, this would have to be another PR.
Okay - this looks good - I misunderstood slightly on first pass. (Thanks for clearing that up for me @adriangb)

I think we also need to be careful to preserve the `RuntimeError("Stream consumed")` case.

I've created #1519 as a similar approach that also handles the stream consumed state.
@tomchristie it looks like this PR should be closed out in favor of #1519 now, correct?
Thank you @nikordaris for his valiant effort on this!! Looking forward to seeing the resolution to this in #1519 
Where is it blocking?

Is there a second request instance involved? (otherwise it should just return, shouldn't it?)
https://github.com/encode/starlette/blob/7eb43757307d4702ee6a1f2739388242c703e47e/starlette/requests.py#L178-L193
Looks like it: https://github.com/encode/starlette/blob/7eb43757307d4702ee6a1f2739388242c703e47e/starlette/middleware/base.py#L26 and https://github.com/encode/starlette/blob/7eb43757307d4702ee6a1f2739388242c703e47e/starlette/routing.py#L38.

Can you write a test for starlette itself?

(just for reference: https://github.com/encode/starlette/pull/498)
"Consume request body in middleware is problematic"

Indeed. Consuming request data in middleware *is* problematic.
Not just to Starlette, but *generally*, everywhere.

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):
```
async def set_body(request: Request, body: bytes):
    async def receive() -> Message:
        return {"type": "http.request", "body": body}

    request._receive = receive

async def get_body(request: Request) -> bytes:
    body = await request.body()
    set_body(request, body)
    return body
```

> 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)

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 `requst.form()` in middleware?
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 `before_request()` in Flask?
https://flask.palletsprojects.com/en/1.1.x/api/#flask.Flask.before_request

@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 `before_request` function, but I'm not very knowledgeable about flask.
@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. 
Is there a way to the request JSON body in this case? It appears to be that I could consume the JSON body in HTTPException and RequestValidationError with add_exception_handler but nothing with ServerErrorMiddleware. Indeed ServerErrorMiddleware has request info but not the JSON Body.

Conversation from Gitter but lmk if anybody have feedbacks
```python
    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
https://github.com/tomwojcik/starlette-context/blob/e222f739f113b74c2dad772d417d7fcc6f82f0ae/examples/example_with_logger/logger.py

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

```python
application.include_router(api_router, prefix=API_V1_STR, dependencies=[Depends(log_json)])
```
is there an update for this?
it seems like this is something a lot of people need for very legit use cases ( mainly logging it seems)

is there a plan to allow consuming the body in a middleware ?
i see the body is cached on the request object _body, is it possible to cache it on the scope so it accessible from everywhere after it is read?

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):

```python
@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 
I use contextmiddleware from from starlette


```
from starlette_context.middleware import ContextMiddleware


class ContextFromMiddleware(ContextMiddleware):
    """
    This class helps in setting a Context for a Request-Response which is missing in Starlette default implementation.
    Initialize an empty dict to write data
    """

    async def set_context(self, request: Request) -> dict:
        return {}
```

You would add the middleware in the app then you could access as _context.data_

@JivanRoquet 
There is a gzip middleware. You could import as 
```
from fastapi.middleware.gzip import GZipMiddleware
```
@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 ](https://fastapi.tiangolo.com/advanced/custom-request-and-route/) does have what you might be looking for.
@JHBalaji yes I've seen that since then, thank you for following up.
I had the same problem but I *also* needed to consume the response. If anyone has this problem here is a solution I used:

```python
from typing import Callable, Awaitable, Tuple, Dict, List

from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response, StreamingResponse
from starlette.types import Scope, Message


class RequestWithBody(Request):
    def __init__(self, scope: Scope, body: bytes) -> None:
        super().__init__(scope, self._receive)
        self._body = body
        self._body_returned = False

    async def _receive(self) -> Message:
        if self._body_returned:
            return {"type": "http.disconnect"}
        else:
            self._body_returned = True
            return {"type": "http.request", "body": self._body, "more_body": False}


class CustomMiddleware(BaseHTTPMiddleware):
    async def dispatch(  # type: ignore
        self, request: Request, call_next: Callable[[Request], Awaitable[StreamingResponse]]
    ) -> Response:
            request_body_bytes = await request.body()
            
            # use request values

            request_with_body = RequestWithBody(request.scope, request_body_bytes)
            response = await call_next(request_with_body)
            response_content_bytes, response_headers, response_status = await self._get_response_params(response)
            
            # use response values

            return Response(response_content_bytes, response_status, response_headers)

    async def _get_response_params(self, response: StreamingResponse) -> Tuple[bytes, Dict[str, str], int]:
        response_byte_chunks: List[bytes] = []
        response_status: List[int] = []
        response_headers: List[Dict[str, str]] = []

        async def send(message: Message) -> None:
            if message["type"] == "http.response.start":
                response_status.append(message["status"])
                response_headers.append({k.decode("utf8"): v.decode("utf8") for k, v in message["headers"]})
            else:
                response_byte_chunks.append(message["body"])

        await response.stream_response(send)
        content = b"".join(response_byte_chunks)
        return content, response_headers[0], response_status[0]

```
After reading through this, it seems the issue isn't with consuming the entire body, starlette actually will pump that async generator and save it to the request for subsequent requests: https://github.com/encode/starlette/blob/e45c5793611bfec606cd9c5d56887ddc67f77c38/starlette/requests.py#L225

The issue comes when getting form data. That function uses the underlying stream, not the body that may or may not be cached already:
https://github.com/encode/starlette/blob/e45c5793611bfec606cd9c5d56887ddc67f77c38/starlette/requests.py#L246

A call to `json()` properly uses the cached data, but `form()` doesn't.

Give this a shot in a controller or middleware:
```python
await request.body()
await request.body()
await request.json()
```
and I bet that works fine. Toss in a `await request.form()` and it will give you that `Stream consumed` error

My work around that I'm using in conjunction with FastAPI:

```python
from collections import Callable
from io import BytesIO

from fastapi.routing import APIRoute
from starlette.datastructures import FormData
from starlette.formparsers import MultiPartParser, FormParser
from starlette.requests import Request
from starlette.responses import Response

try:
    from multipart.multipart import parse_options_header
except ImportError:  # pragma: nocover
    parse_options_header = None


class AsyncIteratorWrapper:
    """
    Small helper to turn BytesIO into async-able iterator
    """

    def __init__(self, bytes_: bytes):
        super().__init__()
        self._it = BytesIO(bytes_)

    def __aiter__(self):
        return self

    async def __anext__(self):
        try:
            value = next(self._it)
        except StopIteration:
            raise StopAsyncIteration
        return value


class PreProcessedFormRequest(Request):
    async def form(self) -> FormData:
        if not hasattr(self, "_form"):
            assert (
                parse_options_header is not None
            ), "The `python-multipart` library must be installed to use form parsing."
            content_type_header = self.headers.get("Content-Type")
            content_type, options = parse_options_header(content_type_header)

            body_iter = AsyncIteratorWrapper(await self.body())
            if content_type == b"multipart/form-data":
                multipart_parser = MultiPartParser(self.headers, body_iter)
                self._form = await multipart_parser.parse()
            elif content_type == b"application/x-www-form-urlencoded":
                form_parser = FormParser(self.headers, body_iter)
                self._form = await form_parser.parse()
            else:
                self._form = FormData()
        return self._form


class PreProcessedFormRoute(APIRoute):
    def get_route_handler(self) -> Callable:
        original_route_handler = super().get_route_handler()

        async def custom_route_handler(request: Request) -> Response:
            request = PreProcessedFormRequest(request.scope, request.receive)
            return await original_route_handler(request)

        return custom_route_handler
```

Then installed into FastAPI like:
```python
app = FastAPI()
app.router.route_class = PreProcessedFormRoute
```
This PR seems like it might fix this actually: https://github.com/encode/starlette/pull/944
> I had the same problem but I _also_ needed to consume the response. If anyone has this problem here is a solution I used:
> 
> ```python
> from typing import Callable, Awaitable, Tuple, Dict, List
> 
> from starlette.middleware.base import BaseHTTPMiddleware
> from starlette.requests import Request
> from starlette.responses import Response, StreamingResponse
> from starlette.types import Scope, Message
> 
> 
> class RequestWithBody(Request):
>     def __init__(self, scope: Scope, body: bytes) -> None:
>         super().__init__(scope, self._receive)
>         self._body = body
>         self._body_returned = False
> 
>     async def _receive(self) -> Message:
>         if self._body_returned:
>             return {"type": "http.disconnect"}
>         else:
>             self._body_returned = True
>             return {"type": "http.request", "body": self._body, "more_body": False}
> 
> 
> class CustomMiddleware(BaseHTTPMiddleware):
>     async def dispatch(  # type: ignore
>         self, request: Request, call_next: Callable[[Request], Awaitable[StreamingResponse]]
>     ) -> Response:
>             request_body_bytes = await request.body()
>             
>             # use request values
> 
>             request_with_body = RequestWithBody(request.scope, request_body_bytes)
>             response = await call_next(request_with_body)
>             response_content_bytes, response_headers, response_status = await self._get_response_params(response)
>             
>             # use response values
> 
>             return Response(response_content_bytes, response_status, response_headers)
> 
>     async def _get_response_params(self, response: StreamingResponse) -> Tuple[bytes, Dict[str, str], int]:
>         response_byte_chunks: List[bytes] = []
>         response_status: List[int] = []
>         response_headers: List[Dict[str, str]] = []
> 
>         async def send(message: Message) -> None:
>             if message["type"] == "http.response.start":
>                 response_status.append(message["status"])
>                 response_headers.append({k.decode("utf8"): v.decode("utf8") for k, v in message["headers"]})
>             else:
>                 response_byte_chunks.append(message["body"])
> 
>         await response.stream_response(send)
>         content = b"".join(response_byte_chunks)
>         return content, response_headers[0], response_status[0]
> ```

It looks like this solution is not working when there are multiple middlewares
@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? 
getting the request body in the middleware feels like something which should work... 
I think we can do it in this way:
1. when you do `Request(scope, receive, send)` it will set self instance into `scope`.
2. next time, when you instantiate a new request object, it will get the request instance from the scope (achievable by implementing `__new__`).
This says we will always have the same `Request` object.
Does `Starlette.exception_handler` count as registering a middleware function? I can't seem to consume the (or access the cached) body in custom exception handlers either.
Same issue, yes.
Thanks for the confirmation, I ended up adapting an idea from [FastAPI](https://fastapi.tiangolo.com/advanced/custom-request-and-route/#accessing-the-request-body-in-an-exception-handler). Essentially my whole app runs as one middleware, and I pulled the same the handler lookup from [`ExceptionMiddleware`](https://github.com/encode/starlette/blob/2ec6db2de6176a34ff5b079c5c3d201ec84c6ad9/starlette/exceptions.py#L78-L101), so that I can pass the same `starlette.Request` to all of my custom handlers. I should be safe this way, since I'm only catching my application's base Exception, everything else will unwind to the built-in exception handlers.
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()
> I think we can do it in this way:
> 
> 1. when you do `Request(scope, receive, send)` it will set self instance into `scope`.
> 2. next time, when you instantiate a new request object, it will get the request instance from the scope (achievable by implementing `__new__`).
>    This says we will always have the same `Request` object.

You mean this? This definitely works and it's pretty smart 😄 

```diff
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?
> @kigawas so this modification didnt merge into master, right?

Check this instead: https://github.com/encode/starlette/issues/1702
@tomchristie   we're currently using the above workaround using the code below:

```
class LoggingMiddleware(BaseHTTPMiddleware):
    def __init__(self, app):
        super().__init__(app)
        self.logger = logging.getLogger()

    async def set_body(self, request):
        receive_ = await request.receive()

        async def receive() -> Message:
            return receive_

        request._receive = receive

    async def dispatch(self, request, call_next):
        req_uuid = request.state.correlation_id
        await self.set_body(request)
        body = await request.body()
        self.logger.info(
            "Request",
            extra={
                "uuid": req_uuid,
                "type": "api-request",
                "method": str(request.method).upper(),
                "url": str(request.url),
                "payload": body.decode("utf-8"),
            },
        )
        start_time = time.time()
        response = await call_next(request)
        process_time = (time.time() - start_time) * 1000
        formatted_process_time = "{0:.2f}".format(process_time)
        self.logger.info(
            "Response sent",
            extra={
                "uuid": req_uuid,
                "type": "api-response",
                "url": f"[{str(request.method).upper()}] {str(request.url)}",
                "code": response.status_code,
                "elapsed_time": f"{formatted_process_time}ms",
            },
        )
        return response
```

this is working great for us.  What would you think about maybe adding some kind of mixin class to starlette?  maybe like a `RequestLoggingMiddleware` class, so that it makes it easier to log request payloads from the API, even though, I agree with you that in general, this is problematic and should be avoided?
> @tomchristie we're currently using the above workaround using the code below:
> 
> ```
> class LoggingMiddleware(BaseHTTPMiddleware):
>     def __init__(self, app):
>         super().__init__(app)
>         self.logger = logging.getLogger()
> 
>     async def set_body(self, request):
>         receive_ = await request.receive()
> 
>         async def receive() -> Message:
>             return receive_
> 
>         request._receive = receive
> 
>     async def dispatch(self, request, call_next):
>         req_uuid = request.state.correlation_id
>         await self.set_body(request)
>         body = await request.body()
>         self.logger.info(
>             "Request",
>             extra={
>                 "uuid": req_uuid,
>                 "type": "api-request",
>                 "method": str(request.method).upper(),
>                 "url": str(request.url),
>                 "payload": body.decode("utf-8"),
>             },
>         )
>         start_time = time.time()
>         response = await call_next(request)
>         process_time = (time.time() - start_time) * 1000
>         formatted_process_time = "{0:.2f}".format(process_time)
>         self.logger.info(
>             "Response sent",
>             extra={
>                 "uuid": req_uuid,
>                 "type": "api-response",
>                 "url": f"[{str(request.method).upper()}] {str(request.url)}",
>                 "code": response.status_code,
>                 "elapsed_time": f"{formatted_process_time}ms",
>             },
>         )
>         return response
> ```
> 
> this is working great for us. What would you think about maybe adding some kind of mixin class to starlette? maybe like a `RequestLoggingMiddleware` class, so that it makes it easier to log request payloads from the API, even though, I agree with you that in general, this is problematic and should be avoided?

This is very similar to our workaround.

```
class LogRequestsMiddleware(BaseHTTPMiddleware):
    """Logs requests and responses.

    Logs contain:
    - Headers
    - Cookies
    - Referer
    - Path
    """

    def __init__(self, app: ASGIApp) -> None:
        super().__init__(app, dispatch=self._get_log_requests_middleware())

    @staticmethod
    def _get_log_requests_middleware() -> Callable:
        async def set_body(request: Request, body: bytes) -> None:
            """
            Sets the request body to the given value that can be parsed.
            Args:
                request:
                body:

            Returns:

            """

            async def receive() -> Message:
                return {"type": "http.request", "body": body}

            request._receive = receive

        async def middleware(request: Request, call_next: Callable) -> Response:
            req_body = await request.body()
            await set_body(request, req_body)
            logger.info("REQUEST: req_body=%s", req_body)
            response: Response = await call_next(request)

            response_body = [chunk async for chunk in response.body_iterator]
            response.body_iterator = iterate_in_threadpool(iter(response_body))
            res_body_json = json.loads(response_body[0].decode())
            logger.info("RESPONSE: res_body_json=%s", res_body_json)
            return response

        return middleware

```
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 `receive` with a function that consumes the request body, nor do I think people are misguided in needing to consume the full request body in a middleware. Sure, it removes the benefits of a streaming architecture, but that's an acceptable trade-off in many cases. For example, in the case of HMAC signature verification: The first thing I want to do when my application receives a request is verify the signature. If the signature is bad, I want the request to be rejected before it uses anymore resources (database connections, logging, etc.).

If I only wrap the `receive` function in another function that does signature verification, then the signature won't be checked until the request has gotten all the way to the endpoint handler where Starlette begins to consume the request body.

The problem comes when you try to consume the request body (i.e., call `receive`) directly inside of the middleware, like this:

```python
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
```

`receive` is now exhausted, and [this line](https://github.com/encode/starlette/blob/882ab6998e9b9ea73b9237552ef295faae3d2c62/starlette/requests.py#L221) hangs because it's waiting for something that will never come.

For me, all that's needed is a tiny, undocumented property on `state` that can signal to Starlette that the body has been consumed and it shouldn't wait for anything else:

```python
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 [`Request.body`](https://github.com/encode/starlette/blob/882ab6998e9b9ea73b9237552ef295faae3d2c62/starlette/requests.py#L233-L239):

```python
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:

```python
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: https://github.com/encode/starlette/pull/1519
> receive is now exhausted, and [this line](https://github.com/encode/starlette/blob/882ab6998e9b9ea73b9237552ef295faae3d2c62/starlette/requests.py#L221) hangs because it's waiting for something that will never come.

You can always insert an empty body message:

```
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
    def wrapped_receive():
        yield {"type": "http.request", "body": b""}
        yield await receive()
    await self.app(scope, wrapped_receive, send)
```

Or something like that


Resolving patch:
diff --git a/starlette/middleware/base.py b/starlette/middleware/base.py
--- a/starlette/middleware/base.py
+++ b/starlette/middleware/base.py
@@ -3,7 +3,7 @@
 import anyio
 
 from starlette.background import BackgroundTask
-from starlette.requests import Request
+from starlette.requests import ClientDisconnect, Request
 from starlette.responses import ContentStream, Response, StreamingResponse
 from starlette.types import ASGIApp, Message, Receive, Scope, Send
 
@@ -14,6 +14,81 @@
 T = typing.TypeVar("T")
 
 
+class _CachedRequest(Request):
+    """
+    If the user calls Request.body() from their dispatch function
+    we cache the entire request body in memory and pass that to downstream middlewares,
+    but if they call Request.stream() then all we do is send an
+    empty body so that downstream things don't hang forever.
+    """
+
+    def __init__(self, scope: Scope, receive: Receive):
+        super().__init__(scope, receive)
+        self._wrapped_rcv_disconnected = False
+        self._wrapped_rcv_consumed = False
+        self._wrapped_rc_stream = self.stream()
+
+    async def wrapped_receive(self) -> Message:
+        # wrapped_rcv state 1: disconnected
+        if self._wrapped_rcv_disconnected:
+            # we've already sent a disconnect to the downstream app
+            # we don't need to wait to get another one
+            # (although most ASGI servers will just keep sending it)
+            return {"type": "http.disconnect"}
+        # wrapped_rcv state 1: consumed but not yet disconnected
+        if self._wrapped_rcv_consumed:
+            # since the downstream app has consumed us all that is left
+            # is to send it a disconnect
+            if self._is_disconnected:
+                # the middleware has already seen the disconnect
+                # since we know the client is disconnected no need to wait
+                # for the message
+                self._wrapped_rcv_disconnected = True
+                return {"type": "http.disconnect"}
+            # we don't know yet if the client is disconnected or not
+            # so we'll wait until we get that message
+            msg = await self.receive()
+            if msg["type"] != "http.disconnect":  # pragma: no cover
+                # at this point a disconnect is all that we should be receiving
+                # if we get something else, things went wrong somewhere
+                raise RuntimeError(f"Unexpected message received: {msg['type']}")
+            return msg
+
+        # wrapped_rcv state 3: not yet consumed
+        if getattr(self, "_body", None) is not None:
+            # body() was called, we return it even if the client disconnected
+            self._wrapped_rcv_consumed = True
+            return {
+                "type": "http.request",
+                "body": self._body,
+                "more_body": False,
+            }
+        elif self._stream_consumed:
+            # stream() was called to completion
+            # return an empty body so that downstream apps don't hang
+            # waiting for a disconnect
+            self._wrapped_rcv_consumed = True
+            return {
+                "type": "http.request",
+                "body": b"",
+                "more_body": False,
+            }
+        else:
+            # body() was never called and stream() wasn't consumed
+            try:
+                stream = self.stream()
+                chunk = await stream.__anext__()
+                self._wrapped_rcv_consumed = self._stream_consumed
+                return {
+                    "type": "http.request",
+                    "body": chunk,
+                    "more_body": not self._stream_consumed,
+                }
+            except ClientDisconnect:
+                self._wrapped_rcv_disconnected = True
+                return {"type": "http.disconnect"}
+
+
 class BaseHTTPMiddleware:
     def __init__(
         self, app: ASGIApp, dispatch: typing.Optional[DispatchFunction] = None
@@ -26,6 +101,8 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
             await self.app(scope, receive, send)
             return
 
+        request = _CachedRequest(scope, receive)
+        wrapped_receive = request.wrapped_receive
         response_sent = anyio.Event()
 
         async def call_next(request: Request) -> Response:
@@ -44,7 +121,7 @@ async def wrap(func: typing.Callable[[], typing.Awaitable[T]]) -> T:
                         return result
 
                     task_group.start_soon(wrap, response_sent.wait)
-                    message = await wrap(request.receive)
+                    message = await wrap(wrapped_receive)
 
                 if response_sent.is_set():
                     return {"type": "http.disconnect"}
@@ -104,9 +181,8 @@ async def body_stream() -> typing.AsyncGenerator[bytes, None]:
             return response
 
         async with anyio.create_task_group() as task_group:
-            request = Request(scope, receive=receive)
             response = await self.dispatch_func(request, call_next)
-            await response(scope, receive, send)
+            await response(scope, wrapped_receive, send)
             response_sent.set()
 
     async def dispatch(
diff --git a/starlette/requests.py b/starlette/requests.py
--- a/starlette/requests.py
+++ b/starlette/requests.py
@@ -216,15 +216,14 @@ async def stream(self) -> typing.AsyncGenerator[bytes, None]:
             return
         if self._stream_consumed:
             raise RuntimeError("Stream consumed")
-        self._stream_consumed = True
-        while True:
+        while not self._stream_consumed:
             message = await self._receive()
             if message["type"] == "http.request":
                 body = message.get("body", b"")
+                if not message.get("more_body", False):
+                    self._stream_consumed = True
                 if body:
                     yield body
-                if not message.get("more_body", False):
-                    break
             elif message["type"] == "http.disconnect":
                 self._is_disconnected = True
                 raise ClientDisconnect()


Test patch:
diff --git a/tests/middleware/test_base.py b/tests/middleware/test_base.py
--- a/tests/middleware/test_base.py
+++ b/tests/middleware/test_base.py
@@ -1,5 +1,6 @@
 import contextvars
 from contextlib import AsyncExitStack
+from typing import AsyncGenerator, Awaitable, Callable, List, Union
 
 import anyio
 import pytest
@@ -7,10 +8,12 @@
 from starlette.applications import Starlette
 from starlette.background import BackgroundTask
 from starlette.middleware import Middleware
-from starlette.middleware.base import BaseHTTPMiddleware
-from starlette.responses import PlainTextResponse, StreamingResponse
+from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
+from starlette.requests import Request
+from starlette.responses import PlainTextResponse, Response, StreamingResponse
 from starlette.routing import Route, WebSocketRoute
-from starlette.types import ASGIApp, Receive, Scope, Send
+from starlette.testclient import TestClient
+from starlette.types import ASGIApp, Message, Receive, Scope, Send
 
 
 class CustomMiddleware(BaseHTTPMiddleware):
@@ -413,3 +416,419 @@ async def downstream_app(scope, receive, send):
     client = test_client_factory(app)
     response = client.get("/does_not_exist")
     assert response.text == "Custom"
+
+
+def test_read_request_stream_in_app_after_middleware_calls_stream(
+    test_client_factory: Callable[[ASGIApp], TestClient]
+) -> None:
+    async def homepage(request: Request):
+        expected = [b""]
+        async for chunk in request.stream():
+            assert chunk == expected.pop(0)
+        assert expected == []
+        return PlainTextResponse("Homepage")
+
+    class ConsumingMiddleware(BaseHTTPMiddleware):
+        async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
+            expected = [b"a", b""]
+            async for chunk in request.stream():
+                assert chunk == expected.pop(0)
+            assert expected == []
+            return await call_next(request)
+
+    app = Starlette(
+        routes=[Route("/", homepage, methods=["POST"])],
+        middleware=[Middleware(ConsumingMiddleware)],
+    )
+
+    client: TestClient = test_client_factory(app)
+    response = client.post("/", content=b"a")
+    assert response.status_code == 200
+
+
+def test_read_request_stream_in_app_after_middleware_calls_body(
+    test_client_factory: Callable[[ASGIApp], TestClient]
+) -> None:
+    async def homepage(request: Request):
+        expected = [b"a", b""]
+        async for chunk in request.stream():
+            assert chunk == expected.pop(0)
+        assert expected == []
+        return PlainTextResponse("Homepage")
+
+    class ConsumingMiddleware(BaseHTTPMiddleware):
+        async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
+            assert await request.body() == b"a"
+            return await call_next(request)
+
+    app = Starlette(
+        routes=[Route("/", homepage, methods=["POST"])],
+        middleware=[Middleware(ConsumingMiddleware)],
+    )
+
+    client: TestClient = test_client_factory(app)
+    response = client.post("/", content=b"a")
+    assert response.status_code == 200
+
+
+def test_read_request_body_in_app_after_middleware_calls_stream(
+    test_client_factory: Callable[[ASGIApp], TestClient]
+) -> None:
+    async def homepage(request: Request):
+        assert await request.body() == b""
+        return PlainTextResponse("Homepage")
+
+    class ConsumingMiddleware(BaseHTTPMiddleware):
+        async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
+            expected = [b"a", b""]
+            async for chunk in request.stream():
+                assert chunk == expected.pop(0)
+            assert expected == []
+            return await call_next(request)
+
+    app = Starlette(
+        routes=[Route("/", homepage, methods=["POST"])],
+        middleware=[Middleware(ConsumingMiddleware)],
+    )
+
+    client: TestClient = test_client_factory(app)
+    response = client.post("/", content=b"a")
+    assert response.status_code == 200
+
+
+def test_read_request_body_in_app_after_middleware_calls_body(
+    test_client_factory: Callable[[ASGIApp], TestClient]
+) -> None:
+    async def homepage(request: Request):
+        assert await request.body() == b"a"
+        return PlainTextResponse("Homepage")
+
+    class ConsumingMiddleware(BaseHTTPMiddleware):
+        async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
+            assert await request.body() == b"a"
+            return await call_next(request)
+
+    app = Starlette(
+        routes=[Route("/", homepage, methods=["POST"])],
+        middleware=[Middleware(ConsumingMiddleware)],
+    )
+
+    client: TestClient = test_client_factory(app)
+    response = client.post("/", content=b"a")
+    assert response.status_code == 200
+
+
+def test_read_request_stream_in_dispatch_after_app_calls_stream(
+    test_client_factory: Callable[[ASGIApp], TestClient]
+) -> None:
+    async def homepage(request: Request):
+        expected = [b"a", b""]
+        async for chunk in request.stream():
+            assert chunk == expected.pop(0)
+        assert expected == []
+        return PlainTextResponse("Homepage")
+
+    class ConsumingMiddleware(BaseHTTPMiddleware):
+        async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
+            resp = await call_next(request)
+            with pytest.raises(RuntimeError, match="Stream consumed"):
+                async for _ in request.stream():
+                    raise AssertionError("should not be called")  # pragma: no cover
+            return resp
+
+    app = Starlette(
+        routes=[Route("/", homepage, methods=["POST"])],
+        middleware=[Middleware(ConsumingMiddleware)],
+    )
+
+    client: TestClient = test_client_factory(app)
+    response = client.post("/", content=b"a")
+    assert response.status_code == 200
+
+
+def test_read_request_stream_in_dispatch_after_app_calls_body(
+    test_client_factory: Callable[[ASGIApp], TestClient]
+) -> None:
+    async def homepage(request: Request):
+        assert await request.body() == b"a"
+        return PlainTextResponse("Homepage")
+
+    class ConsumingMiddleware(BaseHTTPMiddleware):
+        async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
+            resp = await call_next(request)
+            with pytest.raises(RuntimeError, match="Stream consumed"):
+                async for _ in request.stream():
+                    raise AssertionError("should not be called")  # pragma: no cover
+            return resp
+
+    app = Starlette(
+        routes=[Route("/", homepage, methods=["POST"])],
+        middleware=[Middleware(ConsumingMiddleware)],
+    )
+
+    client: TestClient = test_client_factory(app)
+    response = client.post("/", content=b"a")
+    assert response.status_code == 200
+
+
+@pytest.mark.anyio
+async def test_read_request_stream_in_dispatch_wrapping_app_calls_body() -> None:
+    async def endpoint(scope: Scope, receive: Receive, send: Send) -> None:
+        request = Request(scope, receive)
+        async for chunk in request.stream():
+            assert chunk == b"2"
+            break
+        await Response()(scope, receive, send)
+
+    class ConsumingMiddleware(BaseHTTPMiddleware):
+        async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
+            expected = b"1"
+            response: Union[Response, None] = None
+            async for chunk in request.stream():
+                assert chunk == expected
+                if expected == b"1":
+                    response = await call_next(request)
+                    expected = b"3"
+                else:
+                    break
+            assert response is not None
+            return response
+
+    async def rcv() -> AsyncGenerator[Message, None]:
+        yield {"type": "http.request", "body": b"1", "more_body": True}
+        yield {"type": "http.request", "body": b"2", "more_body": True}
+        yield {"type": "http.request", "body": b"3"}
+        await anyio.sleep(float("inf"))
+
+    sent: List[Message] = []
+
+    async def send(msg: Message) -> None:
+        sent.append(msg)
+
+    app: ASGIApp = endpoint
+    app = ConsumingMiddleware(app)
+
+    rcv_stream = rcv()
+
+    await app({"type": "http"}, rcv_stream.__anext__, send)
+
+    assert sent == [
+        {
+            "type": "http.response.start",
+            "status": 200,
+            "headers": [(b"content-length", b"0")],
+        },
+        {"type": "http.response.body", "body": b"", "more_body": False},
+    ]
+
+    await rcv_stream.aclose()
+
+
+def test_read_request_stream_in_dispatch_after_app_calls_body_with_middleware_calling_body_before_call_next(  # noqa: E501
+    test_client_factory: Callable[[ASGIApp], TestClient]
+) -> None:
+    async def homepage(request: Request):
+        assert await request.body() == b"a"
+        return PlainTextResponse("Homepage")
+
+    class ConsumingMiddleware(BaseHTTPMiddleware):
+        async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
+            assert (
+                await request.body() == b"a"
+            )  # this buffers the request body in memory
+            resp = await call_next(request)
+            async for chunk in request.stream():
+                if chunk:
+                    assert chunk == b"a"
+            return resp
+
+    app = Starlette(
+        routes=[Route("/", homepage, methods=["POST"])],
+        middleware=[Middleware(ConsumingMiddleware)],
+    )
+
+    client: TestClient = test_client_factory(app)
+    response = client.post("/", content=b"a")
+    assert response.status_code == 200
+
+
+def test_read_request_body_in_dispatch_after_app_calls_body_with_middleware_calling_body_before_call_next(  # noqa: E501
+    test_client_factory: Callable[[ASGIApp], TestClient]
+) -> None:
+    async def homepage(request: Request):
+        assert await request.body() == b"a"
+        return PlainTextResponse("Homepage")
+
+    class ConsumingMiddleware(BaseHTTPMiddleware):
+        async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
+            assert (
+                await request.body() == b"a"
+            )  # this buffers the request body in memory
+            resp = await call_next(request)
+            assert await request.body() == b"a"  # no problem here
+            return resp
+
+    app = Starlette(
+        routes=[Route("/", homepage, methods=["POST"])],
+        middleware=[Middleware(ConsumingMiddleware)],
+    )
+
+    client: TestClient = test_client_factory(app)
+    response = client.post("/", content=b"a")
+    assert response.status_code == 200
+
+
+@pytest.mark.anyio
+async def test_read_request_disconnected_client() -> None:
+    """If we receive a disconnect message when the downstream ASGI
+    app calls receive() the Request instance passed into the dispatch function
+    should get marked as disconnected.
+    The downstream ASGI app should not get a ClientDisconnect raised,
+    instead if should just receive the disconnect message.
+    """
+
+    async def endpoint(scope: Scope, receive: Receive, send: Send) -> None:
+        msg = await receive()
+        assert msg["type"] == "http.disconnect"
+        await Response()(scope, receive, send)
+
+    class ConsumingMiddleware(BaseHTTPMiddleware):
+        async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
+            response = await call_next(request)
+            disconnected = await request.is_disconnected()
+            assert disconnected is True
+            return response
+
+    scope = {"type": "http", "method": "POST", "path": "/"}
+
+    async def receive() -> AsyncGenerator[Message, None]:
+        yield {"type": "http.disconnect"}
+        raise AssertionError("Should not be called, would hang")  # pragma: no cover
+
+    async def send(msg: Message):
+        if msg["type"] == "http.response.start":
+            assert msg["status"] == 200
+
+    app: ASGIApp = ConsumingMiddleware(endpoint)
+
+    rcv = receive()
+
+    await app(scope, rcv.__anext__, send)
+
+    await rcv.aclose()
+
+
+@pytest.mark.anyio
+async def test_read_request_disconnected_after_consuming_steam() -> None:
+    async def endpoint(scope: Scope, receive: Receive, send: Send) -> None:
+        msg = await receive()
+        assert msg.pop("more_body", False) is False
+        assert msg == {"type": "http.request", "body": b"hi"}
+        msg = await receive()
+        assert msg == {"type": "http.disconnect"}
+        await Response()(scope, receive, send)
+
+    class ConsumingMiddleware(BaseHTTPMiddleware):
+        async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
+            await request.body()
+            disconnected = await request.is_disconnected()
+            assert disconnected is True
+            response = await call_next(request)
+            return response
+
+    scope = {"type": "http", "method": "POST", "path": "/"}
+
+    async def receive() -> AsyncGenerator[Message, None]:
+        yield {"type": "http.request", "body": b"hi"}
+        yield {"type": "http.disconnect"}
+        raise AssertionError("Should not be called, would hang")  # pragma: no cover
+
+    async def send(msg: Message):
+        if msg["type"] == "http.response.start":
+            assert msg["status"] == 200
+
+    app: ASGIApp = ConsumingMiddleware(endpoint)
+
+    rcv = receive()
+
+    await app(scope, rcv.__anext__, send)
+
+    await rcv.aclose()
+
+
+def test_downstream_middleware_modifies_receive(
+    test_client_factory: Callable[[ASGIApp], TestClient]
+) -> None:
+    """If a downstream middleware modifies receive() the final ASGI app
+    should see the modified version.
+    """
+
+    async def endpoint(scope: Scope, receive: Receive, send: Send) -> None:
+        request = Request(scope, receive)
+        body = await request.body()
+        assert body == b"foo foo "
+        await Response()(scope, receive, send)
+
+    class ConsumingMiddleware(BaseHTTPMiddleware):
+        async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
+            body = await request.body()
+            assert body == b"foo "
+            return await call_next(request)
+
+    def modifying_middleware(app: ASGIApp) -> ASGIApp:
+        async def wrapped_app(scope: Scope, receive: Receive, send: Send) -> None:
+            async def wrapped_receive() -> Message:
+                msg = await receive()
+                if msg["type"] == "http.request":
+                    msg["body"] = msg["body"] * 2
+                return msg
+
+            await app(scope, wrapped_receive, send)
+
+        return wrapped_app
+
+    client = test_client_factory(ConsumingMiddleware(modifying_middleware(endpoint)))
+
+    resp = client.post("/", content=b"foo ")
+    assert resp.status_code == 200
+
+
+CallNext = Callable[[Request], Awaitable[Response]]
+
+
+def test_pr_1519_comment_1236166180_example() -> None:
+    """
+    https://github.com/encode/starlette/pull/1519#issuecomment-1236166180
+    """
+    bodies: List[bytes] = []
+
+    class LogRequestBodySize(BaseHTTPMiddleware):
+        async def dispatch(self, request: Request, call_next: CallNext) -> Response:
+            print(len(await request.body()))
+            return await call_next(request)
+
+    def replace_body_middleware(app: ASGIApp) -> ASGIApp:
+        async def wrapped_app(scope: Scope, receive: Receive, send: Send) -> None:
+            async def wrapped_rcv() -> Message:
+                msg = await receive()
+                msg["body"] += b"-foo"
+                return msg
+
+            await app(scope, wrapped_rcv, send)
+
+        return wrapped_app
+
+    async def endpoint(request: Request) -> Response:
+        body = await request.body()
+        bodies.append(body)
+        return Response()
+
+    app: ASGIApp = Starlette(routes=[Route("/", endpoint, methods=["POST"])])
+    app = replace_body_middleware(app)
+    app = LogRequestBodySize(app)
+
+    client = TestClient(app)
+    resp = client.post("/", content=b"Hello, World!")
+    resp.raise_for_status()
+
+    assert bodies == [b"Hello, World!-foo"]
diff --git a/tests/test_requests.py b/tests/test_requests.py
--- a/tests/test_requests.py
+++ b/tests/test_requests.py
@@ -1,5 +1,5 @@
 import sys
-from typing import Optional
+from typing import List, Optional
 
 import anyio
 import pytest
@@ -7,7 +7,7 @@
 from starlette.datastructures import Address
 from starlette.requests import ClientDisconnect, Request, State
 from starlette.responses import JSONResponse, PlainTextResponse, Response
-from starlette.types import Scope
+from starlette.types import Message, Scope
 
 
 def test_request_url(test_client_factory):
@@ -506,3 +506,69 @@ async def app(scope, receive, send):
     client = test_client_factory(app)
     response = client.get("/")
     assert response.json() == {"json": "Send channel not available"}
+
+
+@pytest.mark.parametrize(
+    "messages",
+    [
+        [{"body": b"123", "more_body": True}, {"body": b""}],
+        [{"body": b"", "more_body": True}, {"body": b"123"}],
+        [{"body": b"12", "more_body": True}, {"body": b"3"}],
+        [
+            {"body": b"123", "more_body": True},
+            {"body": b"", "more_body": True},
+            {"body": b""},
+        ],
+    ],
+)
+@pytest.mark.anyio
+async def test_request_rcv(messages: List[Message]) -> None:
+    messages = messages.copy()
+
+    async def rcv() -> Message:
+        return {"type": "http.request", **messages.pop(0)}
+
+    request = Request({"type": "http"}, rcv)
+
+    body = await request.body()
+
+    assert body == b"123"
+
+
+@pytest.mark.anyio
+async def test_request_stream_called_twice() -> None:
+    messages: List[Message] = [
+        {"type": "http.request", "body": b"1", "more_body": True},
+        {"type": "http.request", "body": b"2", "more_body": True},
+        {"type": "http.request", "body": b"3"},
+    ]
+
+    async def rcv() -> Message:
+        return messages.pop(0)
+
+    request = Request({"type": "http"}, rcv)
+
+    s1 = request.stream()
+    s2 = request.stream()
+
+    msg = await s1.__anext__()
+    assert msg == b"1"
+
+    msg = await s2.__anext__()
+    assert msg == b"2"
+
+    msg = await s1.__anext__()
+    assert msg == b"3"
+
+    # at this point we've consumed the entire body
+    # so we should not wait for more body (which would hang us forever)
+    msg = await s1.__anext__()
+    assert msg == b""
+    msg = await s2.__anext__()
+    assert msg == b""
+
+    # and now both streams are exhausted
+    with pytest.raises(StopAsyncIteration):
+        assert await s2.__anext__()
+    with pytest.raises(StopAsyncIteration):
+        await s1.__anext__()