-
-
Notifications
You must be signed in to change notification settings - Fork 949
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
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
Deprecating BaseHTTPMiddleware #1678
Comments
This is a rather bold move to make given the usage of BaseHTTPMiddleware in the wild, so let’s pause and think it through. I’m sure there’s been discussion before (are there any links available?), but jotting down some thoughts and questions… In general, what’s the migration path going to be like for users of BaseHTTPMiddleware? Should we start by exploring how FastAPI could do without? It’s still relying on What does addressing the two problems mentioned in the docs (context vars, background tasks) look like with pure ASGI middleware? What do existing examples in the BaseHTTPMiddleware docs look like using pure ASGI middleware? While I reckon the limitations of BaseHTTPMiddleware, and the fact that currently users expect certain things to work that don’t, I also fear « pure ASGI middleware all the things » might be really off putting a lot of folks who’d like to do very simple edits to requests or responses. |
I agree. I realize this is pretty aggressive proposal and recognize that it's a long shot for it to be accepted, but I hope that it can at least lead to some good discussion.
There are lots of general issues related to BaseHTTPMiddleware, the last/clearest I can think of is #1640 (comment).
I think there are several ways to tackle this:
Basically: nothing. These problems simple don't exist with pure ASGI middleware.
+1 on this. I think we should replace any examples not specific to BaseHTTPMiddleware with pure ASGI middleware, independently of the outcome of this proposal.
I've found that often a pure ASGI middleware isn't necessarily more LOC or that ugly once you know how to write it (I admit there is a learning curve). Things that are ugly with pure ASGI middleware (like intercepting the request or response body byte stream) tend to be impossible or buggy with BaseHTTPMiddleware. Pure ASGI also has the huge ecosystem benefit of not being coupled to Starlette. So maybe pure ASGI all the things isn't that bad. On the other hand, mentioned above, I think we maybe can come up with a different API for these "simple" middlewares that doesn't suffer from the pitfalls of BaseHTTPMiddleware (single example). |
I just looked and we don't have any in our docs, but FastAPI has several. @tiangolo how would you feel about reworking those examples? I'm happy to help. |
Now, if we consider something like #1691, I think a migration path could be:
? |
Hello! We at @onekey-sec have a pretty good understanding of the problems with ProblemsSynchronization issues in previous versionThe previous version had synchrionization issues, for example The current implementation has the following problems: Every coroutine got cancelledafter the line async with anyio.create_task_group() as task_group:
...
task_group.cancel_scope.cancel() This is why Here is another concreate example which doesn't work: from sqlalchemy.async import create_async_engine
class DatabaseMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send):
db_engine = create_async_engine(...)
async with db_engine.begin() as conn: # __aexit__ will be cancelled, the database connection will not be closed properly
scope["state"]["conn"] = conn
await self.app(scope, receive, send)
middlewares = [
Middleware(SomeBaseHTTPMiddleware, ...),
Middleware(DatabaseMiddleware),
]
starlette = Starlette(..., middlewares=middlewares) We figured cancellation might have been put there in case of client disconnections? but it has unfortunate side effects for the whole middleware stack. Exceptions are swallowedafter the line Here is a very surprising example of an Exception you will never see: class NameErrorMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send):
await self.app(scope, receive, send)
this_variable_doesnt_exist # will raise NameError, but you will never see it
middlewares = [
Middleware(SomeBaseHTTPMiddleware, ...),
Middleware(NameErrorMiddleware),
]
starlette = Starlette(..., middlewares=middlewares) Possible solutionsAvoiding lock stepping/synchronization issuesLock stepping with Avoiding cancellationA middleware should not cancel everything underneath it. Background tasks are a good example, so the Providing lower-level toolsfor helping parsing HTTP structures like Headers from ASGI messages can be pretty simple if there are good tooling in the framework. We needed to read some incoming Request headers and modify the response headers, and with the already existing APIs, we could completely get rid of from starlette.datastructures import Headers, MutableHeaders
from starlette.responses import Response
class RequestLoggingMiddleware:
def __init__(self, app: ASGIApp):
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send):
if scope["type"] != "http":
await self.app(scope, receive, send)
return
request_headers = Headers(scope=scope)
try:
request_id = request_headers["X-Request-Id"]
except KeyError:
message = "Missing X-Request-Id header"
logger.exception(message)
error_response = Response(message, status_code=status.HTTP_400_BAD_REQUEST)
await error_response(scope, receive, send)
return
logger.debug("Request received")
def send_wrapper(message: Message):
if message["type"] == "http.response.start":
# This modifies the "message" Dict in place, which is used by the "send" function below
response_headers = MutableHeaders(scope=message)
response_headers["X-Request-Id"] = request_id
return send(message)
await self.app(scope, receive, send_wrapper)
logger.debug("Request finished") Similarly, some helpers for reading and writing the streams in a sychronized manner should also help if someone want's to intercept the HTTP body stream, something like the current Better documentationAbout the documentation, I completely agree with you, some good examples for pure ASGI-middleware, maybe with the lower-level helpers would be better than a complex middleware like this. Hope this helps! |
Thank you for taking the time to dig into the codebase!
These are interesting edge cases that I don't think I'd seen before. I think we should review them more and if they check out add them to the
I didn't understand what the proposed solution / problem is in this section
I think we all agree on this, but the questions is how can we avoid cancellation without braking anything? #1715 may be a viable proposal
These already exist. Is the suggestion to put this into the docs (see below)?
We're working on it! We recently merged #1656 and #1723 is building upon that and adding even more examples (including ones using |
…ct`+`recv_stream.close` (#1715) * replace BaseMiddleware cancellation after request send with closing recv_stream + http.disconnect in receive fixes #1438 * Add no cover pragma on pytest.fail in tests/middleware/test_base.py Co-authored-by: Adrian Garcia Badaracco <[email protected]> * make http_disconnect_while_sending test more robust in the face of scheduling issues * Fix issue with running middleware context manager Reported in #1678 (comment) Co-authored-by: Adrian Garcia Badaracco <[email protected]> Co-authored-by: Marcelo Trylesinski <[email protected]>
…ct`+`recv_stream.close` (#1715) * replace BaseMiddleware cancellation after request send with closing recv_stream + http.disconnect in receive fixes #1438 * Add no cover pragma on pytest.fail in tests/middleware/test_base.py Co-authored-by: Adrian Garcia Badaracco <[email protected]> * make http_disconnect_while_sending test more robust in the face of scheduling issues * Fix issue with running middleware context manager Reported in encode/starlette#1678 (comment) Co-authored-by: Adrian Garcia Badaracco <[email protected]> Co-authored-by: Marcelo Trylesinski <[email protected]>
FYI, by converting to pure ASGI style, I observed 50% of speed improvements compared to the previous one which was inheriting from BaseHTTPMiddleware.
|
Nice! A 50% performance improvement is a lot more than I was expecting, but I would expect a small (probably unmeasurable unless you're piling on dozens of |
Please find the whole code (simplified version of my production code)
from fastapi import FastAPI
from starlette.requests import Request
app = FastAPI()
@app.get("/healthcheck")
async def root():
return {"message": "Hello World"}
@app.middleware("http")
async def request_logging_middleware(request: Request, call_next):
response = await call_next(request)
return response
from fastapi import FastAPI
from starlette.middleware import Middleware
from starlette.types import ASGIApp, Scope, Receive, Send, Message
class RequestLoggingMiddleware:
def __init__(self, app: ASGIApp):
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send):
if scope["type"] != "http":
await self.app(scope, receive, send)
return
def send_wrapper(message: Message):
return send(message)
await self.app(scope, receive, send_wrapper)
app = FastAPI(middleware=[Middleware(RequestLoggingMiddleware)])
@app.get("/healthcheck")
async def root():
return {"message": "Hello World"}
Interesting :) |
I believe most of the information here is not up-to-date, since we fixed some stuff. Which problems does the |
Off the top of my head:
|
My concern here is that those limitations may not be a reason for deprecating the feature. Since for a beginner, it's easier to use a Also, you brought a solution for the "Accessing the request body is problematic" on #1692, right? |
I would never sacrifice correctness for beginner friendliness. asyncio by itself is not beginner friendly at all and I would also argue if a beginner hit these problems, there is no way they will figure it out. |
The async argument makes sense, but the BaseHTTPMiddleware limitations are documented. |
These are not "limitations", but serious bugs. We lost so much time to these and were having bugs in our middleware for months didn't even notice. Starlette is pretty good and useful, but this middleware is so buggy it should never have been in the codebase at all. |
The issues you faced on your previous report do not exist anymore. If you can tell me the "serious bugs" that the |
Do other members have an opinion here? @encode/maintainers |
IIRC, Documentation is a bit confusing right now... Goes on an off topic explanation of Regardless, I see this as a "recommended pattern" or "documentation" issue at this point rather than something that must be removed. (Do we really want to break FastAPI with Starlette 1.0? 😑) The documentation in general is getting too wordy and off topic. Should be short and to the point... It's how Starlette originally got popular. |
What BaseHTTPMiddleware issue? The background tasks issue was already solved. |
It’s not off topic, BaseHTTPMiddleware does behave different from pure ASGI middleware w.r.t. context vars. We even have a test to prove it: starlette/tests/middleware/test_base.py Lines 180 to 211 in 337ae24
Like Marcelo said there was never a single issue with BaseHTTPMiddleware, there’s been many. And |
Ah. So, issues arise when using
Seems tiangolo of FastAPI is aware of this already: #420 (comment) I mean if you're going to break it, 1.0 is the time to do it. 🤷 |
…ct`+`recv_stream.close` (#1715) * replace BaseMiddleware cancellation after request send with closing recv_stream + http.disconnect in receive fixes #1438 * Add no cover pragma on pytest.fail in tests/middleware/test_base.py Co-authored-by: Adrian Garcia Badaracco <[email protected]> * make http_disconnect_while_sending test more robust in the face of scheduling issues * Fix issue with running middleware context manager Reported in #1678 (comment) Co-authored-by: Adrian Garcia Badaracco <[email protected]> Co-authored-by: Marcelo Trylesinski <[email protected]>
Although I still think BaseHTTPMiddleware is not great we have fixed a lot of the issues it had. I’m warming up to the idea of not removing it for 1.0. I still think we should discourage its use, but maybe we can keep it around until 2.0 to ease the burden for migrating to 1.0? Does something like this even make sense or if we’re going to discourage it should we just remove it? |
I think the "Limitations" section that we have is already a way to discourage it. Also, as Florimond said above:
So, I'm happy to keep it. |
I guess that works for now. And it gives us some more time to think about #1691. |
Converting this to a discussion, since it's what it looks. I've already talked to @adriangb about doing it. 👍 |
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
Based on many previous discussions, I am proposing that we deprecate
BaseHTTPMiddleware
.We'll put a deprecation warning in the constructor. In this warning we will give:
BaseHTTPMiddleware
This will be a part of a multipronged approach consisting of:
Better documentation. We've already documented the limitations and issues withThis is now done!BaseHTTPMiddleware
both in our docs and tests. Document how to create ASGI middlewares #1656 will give us better documentation on creating ASGI middleware.I (and I think @Kludex as well) are prepared to personally contribute to downstream packages by porting their middlewares to pure ASGI middleware. We'll probably just look for any packages with > X stars or something and make PRs.We've proposed (and merged some) upstream fixes into some of the most popular libraries usingBaseHTTPMiddleware
we could find on GitHub.The text was updated successfully, but these errors were encountered: