Skip to content

Commit d8262aa

Browse files
committed
Pass the request ctx rather than use the globals in the app
The globals have a performance penalty which can be justified for the convinience in user code. In the app however the ctx can easily be passed through the method calls thereby reducing the performance penalty. This may affect extensions if they have subclassed the app and overridden these methods.
1 parent 74d9235 commit d8262aa

File tree

5 files changed

+80
-42
lines changed

5 files changed

+80
-42
lines changed

CHANGES.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ Unreleased
66
- Remove previously deprecated code. :pr:`5223`
77
- Restructure the code such that the Flask (app) and Blueprint
88
classes have Sans-IO bases. :pr:`5127`
9-
9+
- Pass the request ctx rather than use the globals in the app class
10+
methods. :pr:`5229`
1011

1112
Version 2.3.3
1213
-------------

src/flask/app.py

Lines changed: 72 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -692,12 +692,17 @@ def test_cli_runner(self, **kwargs: t.Any) -> FlaskCliRunner:
692692
return cls(self, **kwargs) # type: ignore
693693

694694
def handle_http_exception(
695-
self, e: HTTPException
695+
self,
696+
e: HTTPException,
697+
ctx: RequestContext,
696698
) -> HTTPException | ft.ResponseReturnValue:
697699
"""Handles an HTTP exception. By default this will invoke the
698700
registered error handlers and fall back to returning the
699701
exception as response.
700702
703+
.. versionchanged:: 3.0
704+
The request context, ctx, is now a required argument.
705+
701706
.. versionchanged:: 1.0.3
702707
``RoutingException``, used internally for actions such as
703708
slash redirects during routing, is not passed to error
@@ -721,13 +726,15 @@ def handle_http_exception(
721726
if isinstance(e, RoutingException):
722727
return e
723728

724-
handler = self._find_error_handler(e, request.blueprints)
729+
handler = self._find_error_handler(e, ctx.request.blueprints)
725730
if handler is None:
726731
return e
727732
return self.ensure_sync(handler)(e)
728733

729734
def handle_user_exception(
730-
self, e: Exception
735+
self,
736+
e: Exception,
737+
ctx: RequestContext,
731738
) -> HTTPException | ft.ResponseReturnValue:
732739
"""This method is called whenever an exception occurs that
733740
should be handled. A special case is :class:`~werkzeug
@@ -736,6 +743,9 @@ def handle_user_exception(
736743
return a response value or reraise the exception with the same
737744
traceback.
738745
746+
.. versionchanged:: 3.0
747+
The request context, ctx, is now a required argument.
748+
739749
.. versionchanged:: 1.0
740750
Key errors raised from request data like ``form`` show the
741751
bad key in debug mode rather than a generic bad request
@@ -749,16 +759,16 @@ def handle_user_exception(
749759
e.show_exception = True
750760

751761
if isinstance(e, HTTPException) and not self.trap_http_exception(e):
752-
return self.handle_http_exception(e)
762+
return self.handle_http_exception(e, ctx)
753763

754-
handler = self._find_error_handler(e, request.blueprints)
764+
handler = self._find_error_handler(e, ctx.request.blueprints)
755765

756766
if handler is None:
757767
raise
758768

759769
return self.ensure_sync(handler)(e)
760770

761-
def handle_exception(self, e: Exception) -> Response:
771+
def handle_exception(self, e: Exception, ctx: RequestContext) -> Response:
762772
"""Handle an exception that did not have an error handler
763773
associated with it, or that was raised from an error handler.
764774
This always causes a 500 ``InternalServerError``.
@@ -775,6 +785,9 @@ def handle_exception(self, e: Exception) -> Response:
775785
always receive the ``InternalServerError``. The original
776786
unhandled exception is available as ``e.original_exception``.
777787
788+
.. versionchanged:: 3.0
789+
The request context, ctx, is now a required argument.
790+
778791
.. versionchanged:: 1.1.0
779792
Always passes the ``InternalServerError`` instance to the
780793
handler, setting ``original_exception`` to the unhandled
@@ -801,77 +814,87 @@ def handle_exception(self, e: Exception) -> Response:
801814

802815
raise e
803816

804-
self.log_exception(exc_info)
817+
self.log_exception(exc_info, ctx)
805818
server_error: InternalServerError | ft.ResponseReturnValue
806819
server_error = InternalServerError(original_exception=e)
807-
handler = self._find_error_handler(server_error, request.blueprints)
820+
handler = self._find_error_handler(server_error, ctx.request.blueprints)
808821

809822
if handler is not None:
810823
server_error = self.ensure_sync(handler)(server_error)
811824

812-
return self.finalize_request(server_error, from_error_handler=True)
825+
return self.finalize_request(server_error, ctx, from_error_handler=True)
813826

814827
def log_exception(
815828
self,
816829
exc_info: (tuple[type, BaseException, TracebackType] | tuple[None, None, None]),
830+
ctx: RequestContext,
817831
) -> None:
818832
"""Logs an exception. This is called by :meth:`handle_exception`
819833
if debugging is disabled and right before the handler is called.
820834
The default implementation logs the exception as error on the
821835
:attr:`logger`.
822836
837+
.. versionchanged:: 3.0
838+
The request context, ctx, is now a required argument.
839+
823840
.. versionadded:: 0.8
824841
"""
825842
self.logger.error(
826-
f"Exception on {request.path} [{request.method}]", exc_info=exc_info
843+
f"Exception on {ctx.request.path} [{ctx.request.method}]", exc_info=exc_info
827844
)
828845

829-
def dispatch_request(self) -> ft.ResponseReturnValue:
846+
def dispatch_request(self, ctx: RequestContext) -> ft.ResponseReturnValue:
830847
"""Does the request dispatching. Matches the URL and returns the
831848
return value of the view or error handler. This does not have to
832849
be a response object. In order to convert the return value to a
833850
proper response object, call :func:`make_response`.
834851
852+
.. versionchanged:: 3.0
853+
The request context, ctx, is now a required argument.
854+
835855
.. versionchanged:: 0.7
836856
This no longer does the exception handling, this code was
837857
moved to the new :meth:`full_dispatch_request`.
838858
"""
839-
req = request_ctx.request
840-
if req.routing_exception is not None:
841-
self.raise_routing_exception(req)
842-
rule: Rule = req.url_rule # type: ignore[assignment]
859+
if ctx.request.routing_exception is not None:
860+
self.raise_routing_exception(ctx.request)
861+
rule: Rule = ctx.request.url_rule # type: ignore[assignment]
843862
# if we provide automatic options for this URL and the
844863
# request came with the OPTIONS method, reply automatically
845864
if (
846865
getattr(rule, "provide_automatic_options", False)
847-
and req.method == "OPTIONS"
866+
and ctx.request.method == "OPTIONS"
848867
):
849868
return self.make_default_options_response()
850869
# otherwise dispatch to the handler for that endpoint
851-
view_args: dict[str, t.Any] = req.view_args # type: ignore[assignment]
870+
view_args: dict[str, t.Any] = ctx.request.view_args # type: ignore[assignment]
852871
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
853872

854-
def full_dispatch_request(self) -> Response:
873+
def full_dispatch_request(self, ctx: RequestContext) -> Response:
855874
"""Dispatches the request and on top of that performs request
856875
pre and postprocessing as well as HTTP exception catching and
857876
error handling.
858877
878+
.. versionchanged:: 3.0
879+
The request context, ctx, is now a required argument.
880+
859881
.. versionadded:: 0.7
860882
"""
861883
self._got_first_request = True
862884

863885
try:
864886
request_started.send(self, _async_wrapper=self.ensure_sync)
865-
rv = self.preprocess_request()
887+
rv = self.preprocess_request(ctx)
866888
if rv is None:
867-
rv = self.dispatch_request()
889+
rv = self.dispatch_request(ctx)
868890
except Exception as e:
869-
rv = self.handle_user_exception(e)
870-
return self.finalize_request(rv)
891+
rv = self.handle_user_exception(e, ctx)
892+
return self.finalize_request(rv, ctx)
871893

872894
def finalize_request(
873895
self,
874896
rv: ft.ResponseReturnValue | HTTPException,
897+
ctx: RequestContext,
875898
from_error_handler: bool = False,
876899
) -> Response:
877900
"""Given the return value from a view function this finalizes
@@ -884,11 +907,14 @@ def finalize_request(
884907
with the `from_error_handler` flag. If enabled, failures in
885908
response processing will be logged and otherwise ignored.
886909
910+
.. versionchanged:: 3.0
911+
The request context, ctx, is now a required argument.
912+
887913
:internal:
888914
"""
889915
response = self.make_response(rv)
890916
try:
891-
response = self.process_response(response)
917+
response = self.process_response(response, ctx)
892918
request_finished.send(
893919
self, _async_wrapper=self.ensure_sync, response=response
894920
)
@@ -1215,7 +1241,7 @@ def make_response(self, rv: ft.ResponseReturnValue) -> Response:
12151241

12161242
return rv
12171243

1218-
def preprocess_request(self) -> ft.ResponseReturnValue | None:
1244+
def preprocess_request(self, ctx: RequestContext) -> ft.ResponseReturnValue | None:
12191245
"""Called before the request is dispatched. Calls
12201246
:attr:`url_value_preprocessors` registered with the app and the
12211247
current blueprint (if any). Then calls :attr:`before_request_funcs`
@@ -1224,13 +1250,16 @@ def preprocess_request(self) -> ft.ResponseReturnValue | None:
12241250
If any :meth:`before_request` handler returns a non-None value, the
12251251
value is handled as if it was the return value from the view, and
12261252
further request handling is stopped.
1253+
1254+
.. versionchanged:: 3.0
1255+
The request context, ctx, is now a required argument.
12271256
"""
1228-
names = (None, *reversed(request.blueprints))
1257+
names = (None, *reversed(ctx.request.blueprints))
12291258

12301259
for name in names:
12311260
if name in self.url_value_preprocessors:
12321261
for url_func in self.url_value_preprocessors[name]:
1233-
url_func(request.endpoint, request.view_args)
1262+
url_func(ctx.request.endpoint, ctx.request.view_args)
12341263

12351264
for name in names:
12361265
if name in self.before_request_funcs:
@@ -1242,11 +1271,14 @@ def preprocess_request(self) -> ft.ResponseReturnValue | None:
12421271

12431272
return None
12441273

1245-
def process_response(self, response: Response) -> Response:
1274+
def process_response(self, response: Response, ctx: RequestContext) -> Response:
12461275
"""Can be overridden in order to modify the response object
12471276
before it's sent to the WSGI server. By default this will
12481277
call all the :meth:`after_request` decorated functions.
12491278
1279+
.. versionchanged:: 3.0
1280+
The request context, ctx, is now a required argument.
1281+
12501282
.. versionchanged:: 0.5
12511283
As of Flask 0.5 the functions registered for after request
12521284
execution are called in reverse order of registration.
@@ -1255,23 +1287,25 @@ def process_response(self, response: Response) -> Response:
12551287
:return: a new response object or the same, has to be an
12561288
instance of :attr:`response_class`.
12571289
"""
1258-
ctx = request_ctx._get_current_object() # type: ignore[attr-defined]
1259-
12601290
for func in ctx._after_request_functions:
12611291
response = self.ensure_sync(func)(response)
12621292

1263-
for name in chain(request.blueprints, (None,)):
1293+
for name in chain(ctx.request.blueprints, (None,)):
12641294
if name in self.after_request_funcs:
12651295
for func in reversed(self.after_request_funcs[name]):
12661296
response = self.ensure_sync(func)(response)
12671297

12681298
if not self.session_interface.is_null_session(ctx.session):
1269-
self.session_interface.save_session(self, ctx.session, response)
1299+
self.session_interface.save_session(
1300+
self, ctx.session, response # type: ignore[arg-type]
1301+
)
12701302

12711303
return response
12721304

12731305
def do_teardown_request(
1274-
self, exc: BaseException | None = _sentinel # type: ignore
1306+
self,
1307+
ctx: RequestContext,
1308+
exc: BaseException | None = _sentinel, # type: ignore
12751309
) -> None:
12761310
"""Called after the request is dispatched and the response is
12771311
returned, right before the request context is popped.
@@ -1290,13 +1324,16 @@ def do_teardown_request(
12901324
request. Detected from the current exception information if
12911325
not passed. Passed to each teardown function.
12921326
1327+
.. versionchanged:: 3.0
1328+
The request context, ctx, is now a required argument.
1329+
12931330
.. versionchanged:: 0.9
12941331
Added the ``exc`` argument.
12951332
"""
12961333
if exc is _sentinel:
12971334
exc = sys.exc_info()[1]
12981335

1299-
for name in chain(request.blueprints, (None,)):
1336+
for name in chain(ctx.request.blueprints, (None,)):
13001337
if name in self.teardown_request_funcs:
13011338
for func in reversed(self.teardown_request_funcs[name]):
13021339
self.ensure_sync(func)(exc)
@@ -1451,10 +1488,10 @@ def wsgi_app(self, environ: dict, start_response: t.Callable) -> t.Any:
14511488
try:
14521489
try:
14531490
ctx.push()
1454-
response = self.full_dispatch_request()
1491+
response = self.full_dispatch_request(ctx)
14551492
except Exception as e:
14561493
error = e
1457-
response = self.handle_exception(e)
1494+
response = self.handle_exception(e, ctx)
14581495
except: # noqa: B001
14591496
error = sys.exc_info()[1]
14601497
raise

src/flask/ctx.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore
398398
if clear_request:
399399
if exc is _sentinel:
400400
exc = sys.exc_info()[1]
401-
self.app.do_teardown_request(exc)
401+
self.app.do_teardown_request(self, exc)
402402

403403
request_close = getattr(self.request, "close", None)
404404
if request_close is not None:

tests/test_reqctx.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -288,8 +288,8 @@ def test_bad_environ_raises_bad_request():
288288
# use a non-printable character in the Host - this is key to this test
289289
environ["HTTP_HOST"] = "\x8a"
290290

291-
with app.request_context(environ):
292-
response = app.full_dispatch_request()
291+
with app.request_context(environ) as ctx:
292+
response = app.full_dispatch_request(ctx)
293293
assert response.status_code == 400
294294

295295

@@ -308,8 +308,8 @@ def index():
308308
# these characters are all IDNA-compatible
309309
environ["HTTP_HOST"] = "ąśźäüжŠßя.com"
310310

311-
with app.request_context(environ):
312-
response = app.full_dispatch_request()
311+
with app.request_context(environ) as ctx:
312+
response = app.full_dispatch_request(ctx)
313313

314314
assert response.status_code == 200
315315

tests/test_subclassing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
def test_suppressed_exception_logging():
77
class SuppressedFlask(flask.Flask):
8-
def log_exception(self, exc_info):
8+
def log_exception(self, exc_info, ctx):
99
pass
1010

1111
out = StringIO()

0 commit comments

Comments
 (0)