-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Experiment a high-level HTTPMiddleware #1691
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
Closed
Closed
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
a8fa47d
Experiment a high-level HTTPMiddleware
florimondmanca 9084939
Refactor, lint
florimondmanca d192a6b
aclosing on py < 3.10
florimondmanca ce67633
Address feedback
florimondmanca cf16303
Address, refactor
florimondmanca 63535a8
Add missing pragma
florimondmanca 4c48d3d
Switch to dispatch(conn) instead of dispatch(scope), coverage
florimondmanca 3b8e091
More fine-tuning
florimondmanca c3bc03e
Merge branch 'master' into fm/http-middleware
adriangb 9e48c1f
Address
florimondmanca b325d2c
Attempt controlling response attrs
florimondmanca c84c9db
Merge branch 'master' into fm/http-middleware
adriangb 48dc754
Merge branch 'master' into fm/http-middleware
adriangb File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
from typing import Any, AsyncGenerator, Callable, Optional, Union | ||
|
||
from .._compat import aclosing | ||
from ..datastructures import Headers | ||
from ..requests import HTTPConnection | ||
from ..responses import Response | ||
from ..types import ASGIApp, Message, Receive, Scope, Send | ||
|
||
# This type hint not exposed, as it exists mostly for our own documentation purposes. | ||
# End users should use one of these type hints explicitly when overriding '.dispatch()'. | ||
_DispatchFlow = Union[ | ||
# Default case: | ||
# response = yield | ||
AsyncGenerator[None, Response], | ||
# Early response and/or error handling: | ||
# if condition: | ||
# yield Response(...) | ||
# return | ||
# try: | ||
# response = yield None | ||
# except Exception: | ||
# yield Response(...) | ||
# else: | ||
# ... | ||
AsyncGenerator[Optional[Response], Response], | ||
] | ||
|
||
|
||
class HTTPMiddleware: | ||
def __init__( | ||
self, | ||
app: ASGIApp, | ||
dispatch: Optional[Callable[[HTTPConnection], _DispatchFlow]] = None, | ||
) -> None: | ||
self.app = app | ||
self._dispatch_func = self.dispatch if dispatch is None else dispatch | ||
|
||
def dispatch(self, __conn: HTTPConnection) -> _DispatchFlow: | ||
raise NotImplementedError( | ||
"No dispatch implementation was given. " | ||
"Either pass 'dispatch=...' to HTTPMiddleware, " | ||
"or subclass HTTPMiddleware and override the 'dispatch()' method." | ||
) | ||
|
||
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: | ||
if scope["type"] != "http": | ||
await self.app(scope, receive, send) | ||
return | ||
|
||
conn = HTTPConnection(scope) | ||
|
||
async with aclosing(self._dispatch_func(conn)) as flow: | ||
# Kick the flow until the first `yield`. | ||
# Might respond early before we call into the app. | ||
maybe_early_response = await flow.__anext__() | ||
|
||
if maybe_early_response is not None: | ||
try: | ||
await flow.__anext__() | ||
except StopAsyncIteration: | ||
pass | ||
else: | ||
raise RuntimeError("dispatch() should yield exactly once") | ||
|
||
await maybe_early_response(scope, receive, send) | ||
return | ||
|
||
10000 | response_started = False | |
|
||
async def wrapped_send(message: Message) -> None: | ||
nonlocal response_started | ||
|
||
if message["type"] == "http.response.start": | ||
response_started = True | ||
|
||
headers = Headers(raw=message["headers"]) | ||
response = _StubResponse( | ||
status_code=message["status"], | ||
media_type=headers.get("content-type"), | ||
) | ||
response.raw_headers = headers.raw | ||
|
||
try: | ||
await flow.asend(response) | ||
except StopAsyncIteration: | ||
pass | ||
else: | ||
raise RuntimeError("dispatch() should yield exactly once") | ||
|
||
message["headers"] = response.raw_headers | ||
|
||
await send(message) | ||
|
||
try: | ||
await self.app(scope, receive, wrapped_send) | ||
except Exception as exc: | ||
if response_started: | ||
raise | ||
|
||
try: | ||
response = await flow.athrow(exc) | ||
except StopAsyncIteration: | ||
response = None | ||
except Exception: | ||
# Exception was not handled, or they raised another one. | ||
raise | ||
|
||
if response is None: | ||
raise RuntimeError( | ||
f"dispatch() handled exception {exc!r}, " | ||
"but no response was returned" | ||
) | ||
|
||
await response(scope, receive, send) | ||
return | ||
|
||
|
||
# This customized stub response helps prevent users from shooting themselves | ||
# in the foot, doing things that don't actually have any effect. | ||
|
||
|
||
class _StubResponse(Response): | ||
def __init__(self, status_code: int, media_type: Optional[str] = None) -> None: | ||
self._status_code = status_code | ||
self._media_type = media_type | ||
self.raw_headers = [] | ||
|
||
@property # type: ignore | ||
def status_code(self) -> int: # type: ignore | ||
return self._status_code | ||
|
||
@status_code.setter | ||
def status_code(self, value: Any) -> None: | ||
raise RuntimeError( | ||
"Setting .status_code in HTTPMiddleware is not supported. " | ||
"If you're writing middleware that requires modifying the response " | ||
"status code or sending another response altogether, please consider " | ||
"writing pure ASGI middleware. " | ||
"See: https://starlette.io/middleware/#pure-asgi-middleware" | ||
) | ||
|
||
@property # type: ignore | ||
def media_type(self) -> Optional[str]: # type: ignore | ||
return self._media_type | ||
|
||
@media_type.setter | ||
def media_type(self, value: Any) -> None: | ||
raise RuntimeError( | ||
"Setting .media_type in HTTPMiddleware is not supported, as it has " | ||
"no effect. If you do need to tweak the response " | ||
"content type, consider: response.headers['Content-Type'] = ..." | ||
) | ||
|
||
@property # type: ignore | ||
def body(self) -> bytes: # type: ignore | ||
raise RuntimeError( | ||
"Accessing the response body in HTTPMiddleware is not supported. " | ||
"If you're writing middleware that requires peeking into the response " | ||
"body, please consider writing pure ASGI middleware and wrapping send(). " | ||
"See: https://starlette.io/middleware/#pure-asgi-middleware" | ||
) | ||
|
||
@body.setter | ||
def body(self, body: bytes) -> None: | ||
raise RuntimeError( | ||
"Setting the response body in HTTPMiddleware is not supported." | ||
"If you're writing middleware that requires modifying the response " | ||
"body, please consider writing pure ASGI middleware and wrapping send(). " | ||
"See: https://starlette.io/middleware/#pure-asgi-middleware" | ||
) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.