Issue 1:
Currently, using app.test_client() together with multi-threaded environment will fail:

from concurrent.futures import ThreadPoolExecutor

from flask import Flask, jsonify

app = Flask(__name__)

@app.route("/info")
def info():
return jsonify({"status": "success"})

app.testing = True
with app.test_client() as client:

def check(*args):
return client.get("/info")

with ThreadPoolExecutor(5) as tpe:
print(tpe.map(check, range(5)))

with

Traceback (most recent call last):
File "/Users/X/.pyenv/versions/flow-venv-3.9/lib/python3.9/site-packages/flask/ctx.py", line 399, in pop
self.app.do_teardown_request(exc)
File "/Users/X/.pyenv/versions/flow-venv-3.9/lib/python3.9/site-packages/flask/app.py", line 2370, in do_teardown_request
for name in chain(request.blueprints, (None,)):
File "/Users/X/.pyenv/versions/flow-venv-3.9/lib/python3.9/site-packages/werkzeug/local.py", line 316, in __get__
obj = instance._get_current_object() # type: ignore[misc]
File "/Users/X/.pyenv/versions/flow-venv-3.9/lib/python3.9/site-packages/werkzeug/local.py", line 513, in _get_current_object
raise RuntimeError(unbound_message) from None
RuntimeError: Working outside of request context.

Obviously, this can be solved by running the app as standalone (see http://www.prschmid.com/2013/01/multi-threaded-unit-test-for-flask-rest.html), but then I have to add manual error control and re-rise and do not get proper tracebacks while testing, and code coverage is also not given.

Issue 2:
The following code defines a simple app, and queries it from a test that uses threads:

import threading

from flask import Flask
import pytest

def create_app():
app = Flask(__name__)

@app.route("/hi", methods=["POST"])
def hello():
return "Hello, World!"

return app

@pytest.fixture()
def app():
app = create_app()
yield app

@pytest.fixture()
def client(app):
with app.test_client() as client:
yield client

def test_request_example(client):
successes = 0

def f():
nonlocal successes
response = client.post("/hi")
assert "Hello, World!" == response.text
successes += 1

thread = threading.Thread(target=f)
thread.start()
thread.join()

f()

assert successes == 2

When running with pytest on Flask 2.1.3, this test passes:

========================================================================= test session starts ==========================================================================
platform linux -- Python 3.9.2, pytest-7.1.2, pluggy-0.13.0
rootdir: /home/dev/swh-environment/swh-indexer, configfile: pytest.ini
plugins: asyncio-0.18.3, requests-mock-1.9.3, django-4.5.2, xdist-2.5.0, swh.core-2.13, mock-3.8.2, django-test-migrations-1.2.0, flask-1.2.0, redis-2.4.0, hypothesis-6.49.1, cov-3.0.0, subtesthack-0.1.2, forked-1.3.0, postgresql-3.1.3, swh.journal-0.8.1.dev3+gf92d4ac
asyncio: mode=strict
collected 1 item

test_flask_threads.py . [100%]

===================================================================== 1 passed, 1 warning in 0.02s =====================================================================

However, when running with Flask 2.2.0:

========================================================================= test session starts ==========================================================================
platform linux -- Python 3.9.2, pytest-7.1.2, pluggy-0.13.0
rootdir: /home/dev/swh-environment/swh-indexer, configfile: pytest.ini
plugins: asyncio-0.18.3, requests-mock-1.9.3, django-4.5.2, xdist-2.5.0, swh.core-2.13, mock-3.8.2, django-test-migrations-1.2.0, flask-1.2.0, redis-2.4.0, hypothesis-6.49.1, cov-3.0.0, subtesthack-0.1.2, forked-1.3.0, postgresql-3.1.3, swh.journal-0.8.1.dev3+gf92d4ac
asyncio: mode=strict
collected 1 item

test_flask_threads.py F [100%]

=============================================================================== FAILURES ===============================================================================
_________________________________________________________________________ test_request_example _________________________________________________________________________

self = <contextlib.ExitStack object at 0x7fc444b63b20>
exc_details = (<class 'ValueError'>, ValueError("<Token var=<ContextVar name='flask.request_ctx' at 0x7fc44844a1d0> at 0x7fc444a18a80> was created in a different Context"), <traceback object at 0x7fc444a18d40>)
received_exc = False, _fix_exception_context = <function ExitStack.__exit__.<locals>._fix_exception_context at 0x7fc444a7edc0>, suppressed_exc = False

def __exit__(self, *exc_details):
received_exc = exc_details[0] is not None

# We manipulate the exception state so it behaves as though
# we were actually nesting multiple with statements
frame_exc = sys.exc_info()[1]
def _fix_exception_context(new_exc, old_exc):
# Context may not be correct, so find the end of the chain
while 1:
exc_context = new_exc.__context__
if exc_context is old_exc:
# Context is already set correctly (see issue 20317)
return
if exc_context is None or exc_context is frame_exc:
break
new_exc = exc_context
# Change the end of the chain to point to the exception
# we expect it to reference
new_exc.__context__ = old_exc

# Callbacks are invoked in LIFO order to match the behaviour of
# nested context managers
suppressed_exc = False
pending_raise = False
while self._exit_callbacks:
is_sync, cb = self._exit_callbacks.pop()
assert is_sync
try:
> if cb(*exc_details):

/usr/lib/python3.9/contextlib.py:498:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <flask.ctx.AppContext object at 0x7fc444b75730>, exc_type = None, exc_value = None, tb = None

def __exit__(
self,
exc_type: t.Optional[type],
exc_value: t.Optional[BaseException],
tb: t.Optional[TracebackType],
) -> None:
> self.pop(exc_value)

../../.local/lib/python3.9/site-packages/flask/ctx.py:275:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <flask.ctx.AppContext object at 0x7fc444b75730>, exc = None

def pop(self, exc: t.Optional[BaseException] = _sentinel) -> None: # type: ignore
"""Pops the app context."""
try:
if len(self._cv_tokens) == 1:
if exc is _sentinel:
exc = sys.exc_info()[1]
self.app.do_teardown_appcontext(exc)
finally:
ctx = _cv_app.get()
> _cv_app.reset(self._cv_tokens.pop())
E ValueError: <Token var=<ContextVar name='flask.app_ctx' at 0x7fc44844f090> at 0x7fc444a18ac0> was created in a different Context

../../.local/lib/python3.9/site-packages/flask/ctx.py:256: ValueError

During handling of the above exception, another exception occurred:

client = <FlaskClient <Flask 'test_flask_threads'>>

def test_request_example(client):
successes = 0

def f():
nonlocal successes
response = client.post("/hi")
assert "Hello, World!" == response.text
successes += 1

thread = threading.Thread(target=f)
thread.start()
thread.join()

> f()

test_flask_threads.py:42:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
test_flask_threads.py:34: in f
response = client.post("/hi")
../../.local/lib/python3.9/site-packages/werkzeug/test.py:1140: in post
return self.open(*args, **kw)
../../.local/lib/python3.9/site-packages/flask/testing.py:221: in open
self._context_stack.close()
/usr/lib/python3.9/contextlib.py:521: in close
self.__exit__(None, None, None)
/usr/lib/python3.9/contextlib.py:513: in __exit__
raise exc_details[1]
/usr/lib/python3.9/contextlib.py:498: in __exit__
if cb(*exc_details):
../../.local/lib/python3.9/site-packages/flask/ctx.py:432: in __exit__
self.pop(exc_value)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <RequestContext 'http://localhost/hi' [POST] of test_flask_threads>
exc = ValueError("<Token var=<ContextVar name='flask.app_ctx' at 0x7fc44844f090> at 0x7fc444a18ac0> was created in a different Context")

def pop(self, exc: t.Optional[BaseException] = _sentinel) -> None: # type: ignore
"""Pops the request context and unbinds it by doing that. This will
also trigger the execution of functions registered by the
:meth:`~flask.Flask.teardown_request` decorator.

.. versionchanged:: 0.9
Added the `exc` argument.
"""
clear_request = len(self._cv_tokens) == 1

try:
if clear_request:
if exc is _sentinel:
exc = sys.exc_info()[1]
self.app.do_teardown_request(exc)

request_close = getattr(self.request, "close", None)
if request_close is not None:
request_close()
finally:
ctx = _cv_request.get()
token, app_ctx = self._cv_tokens.pop()
> _cv_request.reset(token)
E ValueError: <Token var=<ContextVar name='flask.request_ctx' at 0x7fc44844a1d0> at 0x7fc444a18a80> was created in a different Context

../../.local/lib/python3.9/site-packages/flask/ctx.py:407: ValueError
======================================================================= short test summary info ========================================================================
FAILED test_flask_threads.py::test_request_example - ValueError: <Token var=<ContextVar name='flask.request_ctx' at 0x7fc44844a1d0> at 0x7fc444a18a80> was created in...
===================================================================== 1 failed, 1 warning in 0.11s =====================================================================

Environment:

Python version: 3.9

Flask version: 2.2.0