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

incompatible with flask 2 and async #158

Open
abulka opened this issue Jun 13, 2021 · 4 comments
Open

incompatible with flask 2 and async #158

abulka opened this issue Jun 13, 2021 · 4 comments

Comments

@abulka
Copy link

abulka commented Jun 13, 2021

It seems that flask-debugtoolbar is incompatible with flask 2 async endpoints.

Repro:

Run this flask project (app.py) and hit /data to trigger an async call - which works.

Then uncomment the line enabling the DebugToolbarExtension and we get an exception.

import asyncio
from flask import Flask
from flask import render_template
from icecream import ic
from flask_debugtoolbar import DebugToolbarExtension

app = Flask(__name__)

app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0
app.config['JSON_SORT_KEYS'] = False
app.secret_key = 'super secret key'

# flask debug toolbar - https://flask-debugtoolbar.readthedocs.io/en/latest/index.html#configuration
app.debug = True
app.config['DEBUG_TB_INTERCEPT_REDIRECTS'] = False
app.config['DEBUG_TB_TEMPLATE_EDITOR_ENABLED'] = True
# toolbar = DebugToolbarExtension(app)  # <<<< UNCOMMENT THIS AND WATCH IT CRASH

@app.route('/')
def root():
    return "hi"


async def async_get_data():
    ic('in async function')
    await asyncio.sleep(2)
    ic('out of async function')
    return 'Done!'


@app.route("/data")
async def get_data():
    data = await async_get_data()
    return data
@renegadevi
Copy link

renegadevi commented Jul 24, 2021

Same issue when using async. I am getting this error message.

Traceback (most recent call last):
  File "/usr/local/lib/python3.9/site-packages/flask/app.py", line 2073, in wsgi_app
    response = self.handle_exception(e)
  File "/usr/local/lib/python3.9/site-packages/flask/app.py", line 2070, in wsgi_app
    response = self.full_dispatch_request()
  File "/usr/local/lib/python3.9/site-packages/flask/app.py", line 1516, in full_dispatch_request
    return self.finalize_request(rv)
  File "/usr/local/lib/python3.9/site-packages/flask/app.py", line 1535, in finalize_request
    response = self.make_response(rv)
  File "/usr/local/lib/python3.9/site-packages/flask/app.py", line 1727, in make_response
    raise TypeError(
TypeError: The view function did not return a valid response. The return type must be a string, dict, tuple, Response instance, or WSGI callable, but it was a coroutine.

Edit: Did work (sometimes) if wrap it inside a render_template(), but for other responses like json, instead of ignore it like it should, it tries to run it anyway if using async.

@foarsitter
Copy link

foarsitter commented Oct 27, 2021

The flask-debugtoolbar has its own dispatch_request method which implementation differs from the Flask version.

https://github.com/pallets/flask/blob/44bc286c03ff3f8e783b4f79f75eb3a464940ca0/src/flask/app.py#L1480-L1502

    def dispatch_request(self) -> ResponseReturnValue:
        """Does the request dispatching.  Matches the URL and returns the
        return value of the view or error handler.  This does not have to
        be a response object.  In order to convert the return value to a
        proper response object, call :func:`make_response`.

        .. versionchanged:: 0.7
           This no longer does the exception handling, this code was
           moved to the new :meth:`full_dispatch_request`.
        """
        req = _request_ctx_stack.top.request
        if req.routing_exception is not None:
            self.raise_routing_exception(req)
        rule = req.url_rule
        # if we provide automatic options for this URL and the
        # request came with the OPTIONS method, reply automatically
        if (
            getattr(rule, "provide_automatic_options", False)
            and req.method == "OPTIONS"
        ):
            return self.make_default_options_response()
        # otherwise dispatch to the handler for that endpoint
        return self.ensure_sync(self.view_functions[rule.endpoint])(**req.view_args)

https://github.com/flask-debugtoolbar/flask-debugtoolbar/blob/d474a6a689be916d65c2adf173e6517290902abe/flask_debugtoolbar/__init__.py#L117-L137

I'm not an expert on this but I guess calling ensure_sync in dispatch_request will solve this.

@jeffwidman
Copy link
Member

Happy to merge a PR if you or anyone else wants to dig into it.

On the surface, it looks relatively straightforward--port the updated version from Flask itself into here, along with the update to call process_view, but I'm sure there's a wrinkle or two to keep things interesting.

My current work is unrelated to Flask, so won't have time to look into it myself anytime soon.

@caarmen
Copy link

caarmen commented Aug 24, 2024

I've quickly tried the following hack/changes. Seems to work:

1️⃣ override DebugToolbarExtension to handle coroutines:

from asyncio import iscoroutinefunction
from asgiref.sync import async_to_sync
from flask_debugtoolbar import DebugToolbarExtension

class MyDebugToolbarExtension(DebugToolbarExtension):
    def process_view(self, app, view_func, view_kwargs):
        processed_view = super().process_view(app, view_func, view_kwargs)
        if iscoroutinefunction(processed_view):
            processed_view = async_to_sync(processed_view)
        return processed_view

2️⃣ Enable sql query recording of an async engine:

from flask_sqlalchemy import record_queries
from sqlalchemy.ext.asyncio import create_async_engine


engine = create_async_engine(db_url)
record_queries._listen(engine.sync_engine)

Note that this is calling a "private" function, _listen in the record_queries module.

Disclaimer: I've only just discovered flask-sqlalchemy and flask-debugtoolbar, so there may be better ways of doing this!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

5 participants