Skip to content
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

Do not send empty data to StreamingResponse on BaseHTTPMiddleware #1609

Merged
merged 6 commits into from
May 6, 2022

Conversation

Kludex
Copy link
Sponsor Member

@Kludex Kludex commented Apr 29, 2022

@Kludex Kludex requested a review from abersheeran April 29, 2022 19:55
@Kludex
Copy link
Sponsor Member Author

Kludex commented Apr 29, 2022

@hiranp @abersheeran Can you confirm that the issue is solved with this PR? 🙏

@Kludex Kludex added the bug Something isn't working label Apr 29, 2022
@Kludex Kludex added this to the Version 0.21.0 milestone Apr 29, 2022
@Kludex Kludex requested a review from a team May 2, 2022 11:54
@Kludex
Copy link
Sponsor Member Author

Kludex commented May 6, 2022

Thanks @abersheeran and @adriangb :)

@Kludex Kludex merged commit b8ea367 into master May 6, 2022
@Kludex Kludex deleted the no-more-body-2 branch May 6, 2022 06:03
@MrSalman333
Copy link

Hello,

this PR breaks down stream FastApi >_<

the tare down in the decencies in FastApi doesn't work as expected after it .

@adriangb
Copy link
Member

adriangb commented Nov 8, 2022

Do you have a minimal reproducible example? Keep in mind that FastAPI uses a pinned Starlette version.

@MrSalman333
Copy link

MrSalman333 commented Nov 8, 2022

yah , I'm typing a complete example now ,,,

FastAPI just updated Starlette version this is why we found the issue ,,

EDIT:

import logging

from fastapi import Depends, FastAPI
from fastapi.middleware.base import BaseHTTPMiddleware
from starlette.middleware import Middleware
from starlette.middleware.base import BaseHTTPMiddleware


class CustomMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        return await call_next(request)


app = FastAPI(middleware=[Middleware(CustomMiddleware)])


def dependency_a():
    yield "A"
    logging.warning("WILL NOT RUN")


def dependency_b():
    yield "B"
    logging.warning("WILL NOT RUN")


def dependency_c():
    yield "C"
    logging.warning("WILL RUN")


@app.get(
    "/issue",
    dependencies=[
        Depends(dependency_a),
    ],
)
def show_dependencies_issue(
    b=Depends(dependency_b),
    c=Depends(dependency_c),
):
    return {"status": "ok"}

ok so if I remove the CustomMiddleware from the app. all warning messages will log.
but with the code above ,, only the message with "WILL RUN" will run ,, because it is the last decencies

this issue disappear if I revert this change.
but I'm sure it will cause other issues,

@MrSalman333
Copy link

example added

@adriangb
Copy link
Member

adriangb commented Nov 8, 2022

Try this:

import logging

import anyio
import httpx
from fastapi import Depends, FastAPI, Response
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.middleware import Middleware


class CustomMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        return await call_next(request)


app = FastAPI(middleware=[Middleware(CustomMiddleware)])


def dependency_a(response: Response):
    try:
        yield "A"
    finally:
        logging.warning("A")


def dependency_b(response: Response):
    try:
        yield "B"
    finally:
        logging.warning("B")


def dependency_c(response: Response):
    try:
        yield "C"
    finally:
        logging.warning("C")


@app.get(
    "/issue",
    dependencies=[
        Depends(dependency_a),
    ],
)
def show_dependencies_issue(
    b=Depends(dependency_b),
    c=Depends(dependency_c),
):
    return {"status": "ok"}


async def main() -> None:
    async with httpx.AsyncClient(app=app, base_url="http://example.com") as client:
        await client.get("/issue")

anyio.run(main)

What version of FastAPI does your example work in?

@Kludex
Copy link
Sponsor Member Author

Kludex commented Nov 8, 2022

I can reproduce the issue.

I've isolated the commit, it's confirmed. This changes the behavior.

@MrSalman333
Copy link

lates version[0.86.0] "the current master"

but the issue should show up on > 0.85, because they updated Starlette version to [0.20.4]

@Kludex
Copy link
Sponsor Member Author

Kludex commented Nov 8, 2022

Let's understand if this commit has an issue or if the problem is the FastAPI assumptions around dependencies.

I'll be available to check this tonight (Amsterdam time), jfyk.

@MrSalman333
Copy link

take a look at this file from FastAPI I think u can get there working assumptions

https://github.com/tiangolo/fastapi/blob/master/fastapi/applications.py

check their override of build_middleware_stack and the comment in it

@MrSalman333
Copy link

I'll open a PR on FastAPI that fixes the issue from there side ,, but it will change the behavior of contextvars on FastAPI so not sure if it will get accepted,

just letting you know

@Kludex
Copy link
Sponsor Member Author

Kludex commented Nov 8, 2022

Try this:

import logging

import anyio
import httpx
from fastapi import Depends, FastAPI, Response
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.middleware import Middleware


class CustomMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        return await call_next(request)


app = FastAPI(middleware=[Middleware(CustomMiddleware)])


def dependency_a(response: Response):
    try:
        yield "A"
    finally:
        logging.warning("A")


def dependency_b(response: Response):
    try:
        yield "B"
    finally:
        logging.warning("B")


def dependency_c(response: Response):
    try:
        yield "C"
    finally:
        logging.warning("C")


@app.get(
    "/issue",
    dependencies=[
        Depends(dependency_a),
    ],
)
def show_dependencies_issue(
    b=Depends(dependency_b),
    c=Depends(dependency_c),
):
    return {"status": "ok"}


async def main() -> None:
    async with httpx.AsyncClient(app=app, base_url="http://example.com") as client:
        await client.get("/issue")

anyio.run(main)

What version of FastAPI does your example work in?

Hmmm... This example shows that the dependency is not teardown-ed. Using this code with uvicorn, you can see the logs on SIGINT.

@adriangb
Copy link
Member

adriangb commented Nov 8, 2022

Hmmm... This example shows that the dependency is not teardown-ed. Using this code with uvicorn, you can see the logs on SIGINT.

When I run this on fastapi==0.86.0 and starlette==0.20.4 I see all 3 logs emitted. With the same versions the original example does indeed give me only WARNING:root:WILL RUN.

@Kludex
Copy link
Sponsor Member Author

Kludex commented Nov 8, 2022

Hmmm... This example shows that the dependency is not teardown-ed. Using this code with uvicorn, you can see the logs on SIGINT.

When I run this on fastapi==0.86.0 and starlette==0.20.4 I see all 3 logs emitted. With the same versions the original example does indeed give me only WARNING:root:WILL RUN.

I didn't check this scenario. I'll check later on.

@MrSalman333
Copy link

MrSalman333 commented Nov 8, 2022

Hmmm... This example shows that the dependency is not teardown-ed. Using this code with uvicorn, you can see the logs on SIGINT.

When I run this on fastapi==0.86.0 and starlette==0.20.4 I see all 3 logs emitted. With the same versions the original example does indeed give me only WARNING:root:WILL RUN.

running this with uvicorn, still shows the issue

in our project the test client will not show the same behavior, in the tests this works with no issue

so using try ... finally with AsyncClient will not show this issue

@Kludex
Copy link
Sponsor Member Author

Kludex commented Nov 8, 2022

The problem is only with def dependencies.

I think we know where the issue is... I'll check.

@Kludex
Copy link
Sponsor Member Author

Kludex commented Nov 8, 2022

From AsyncExitStackMiddleware:

image

Traceback (most recent call last):
  File "/home/marcelo/Development/fork/fastapi/fastapi/middleware/asyncexitstack.py", line 18, in __call__
    await self.app(scope, receive, send)
  File "/home/marcelo/.pyenv/versions/fastapi3.7/lib/python3.7/site-packages/starlette/routing.py", line 680, in __call__
    await route.handle(scope, receive, send)
  File "/home/marcelo/.pyenv/versions/fastapi3.7/lib/python3.7/site-packages/starlette/routing.py", line 275, in handle
    await self.app(scope, receive, send)
  File "/home/marcelo/.pyenv/versions/fastapi3.7/lib/python3.7/site-packages/starlette/routing.py", line 68, in app
    await response(scope, receive, send)
  File "/home/marcelo/.pyenv/versions/fastapi3.7/lib/python3.7/site-packages/starlette/responses.py", line 167, in __call__
    await send({"type": "http.response.body", "body": self.body})
  File "/home/marcelo/.pyenv/versions/fastapi3.7/lib/python3.7/site-packages/starlette/middleware/exceptions.py", line 61, in sender
    await send(message)
  File "/home/marcelo/.pyenv/versions/fastapi3.7/lib/python3.7/site-packages/anyio/streams/memory.py", line 215, in send
    await send_event.wait()
  File "/home/marcelo/.pyenv/versions/fastapi3.7/lib/python3.7/site-packages/anyio/_backends/_asyncio.py", line 1843, in wait
    await checkpoint()
  File "/home/marcelo/.pyenv/versions/fastapi3.7/lib/python3.7/site-packages/anyio/_backends/_asyncio.py", line 515, in checkpoint
    await sleep(0)
  File "/home/marcelo/.pyenv/versions/3.7-dev/lib/python3.7/asyncio/tasks.py", line 585, in sleep
    await __sleep0()
  File "/home/marcelo/.pyenv/versions/3.7-dev/lib/python3.7/asyncio/tasks.py", line 579, in __sleep0
    yield
concurrent.futures._base.CancelledError

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Gzip Middleware content-length is incorrect
5 participants