flask 流式响应 RuntimeError: working outside of request context

1、问题

最近要实现这样一个功能:某个 cgi 处理会很耗时,需要把处理的结果实时的反馈给前端,而不能等到后台全完成了再咔一下全扔前端,那样的用户体验谁都没法接受。

web 框架选的 flask,这个比较轻量级,看了下官方文档,恰好有个叫 Streaming from Templates 的功能:

http://flask.pocoo.org/docs/patterns/streaming/#streaming-from-templates

可以满足需求,它以 generate yield 为基础,流式的返回数据到前端。看了下官方的例子貌似很简单,一笔带过,我又搜了下 stackoverflow,上面有个老外给了个更加详尽的例子:Streaming data with Python and Flask

http://stackoverflow.com/questions/13386681/streaming-data-with-python-and-flask

文中的答案没有前后端的数据交互过程,那我就根据自己的需求加个 http 的交互过程了:

@app.route('/username', methods=['GET', 'POST'])
def index():
    req =request
    print req
    print "111------------"  + req.method + "\n"
    def ggg1(req):
        print req  # the req not my pass into the req....
        print "444------------" + req.method + "\n"
        if req.method == 'POST':
            if request.form['username']:
                urlList = request.form['username'].splitlines()
                i = 0
                for url in urlList():
                    i += 1
                    resultStr = url
                    print i, resultStr
                    yield i, resultStr
    print req
    print "222------------" + req.method + "\n"
    return Response(stream_template('index.html', data=ggg1(req)))

好吧,这么一加,噩梦就开始了。。。奇葩的问题出现了:

要么第 5 行和第 8 行不等,要么就是第 9 行报错:

 if request.method == 'POST':  # RuntimeError: working outside of request context

继续在 stackoverflow 上搜索,发现有人遇到了同样的问题,得到的建议是在调用前声明一个 request 上下文:

with app.test_request_context('/username', method='GET'):
    index()

折腾了老半天,还是依旧报错:RuntimeError: working outside of request context

看起来似乎是在进入迭代器以前,原本的 request 的生命周期就已经结束了,因此就没办法再调用了。

那么要解决就有 2 种办法了:

(1)在进入 generationFunc 前将请求复制一份保存下来以供 generationFunc 调用。

(2)利用 app.test_request_context 创建的是一个全新的 request,将数据传给 generationFunc 使用。

以上这两种办法都曾试过,但是由于理解上的偏差,导致一直未能成功。后来经过 坚实 同学的指点,才明白个中缘由,问题得以解决。

2、解决方案

(1)复制 request

将请求复制下来但不能直接 req = request 这种形式,这只是给 request 取了个别名,它们是共享引用。正确的代码如下:

from flask.ctx import _request_ctx_stack
global new_request
@app.route('/')
@app.route('/demo', methods=['POST'])
def index():
    ctx = _request_ctx_stack.top.copy()
    new_request = ctx.request
    def generateFunc():
        if new_request.method == 'POST':
            if new_request.form['digitValue']:
                num = int(new_request.form['digitValue'])
                i = 0
                for n in xrange(num):
                    i += 1
                    print "%s:\t%s" % (i, n)
                    yield i, n

    return Response(stream_template('index.html', data=generateFunc()))

PS: 其实像 _request_ctx_stack 这种以下划线开头的变量属于私有变量,外部是不应该调用的,不过坚实同学暂时也没有找到其他能正式调用到它的方法 ,就先这么用着吧。

(2)构造全新 request

上面的这种写法:with app.test_request_context('/username', method='GET'):

之所以不可以是因为 app.test_request_context 创建的是一个全新的 request,它包含的 url, method, headers, form 值都是要在创建时自定义的,它不会把原来的 request 里的数据带进来,需要自己传进去,类似这样:

with app.test_request_context('/demo', method='POST', data=request.form) as new_context:
        def generateFunc():

PS: test_request_context 应该是做单元测试用的,用来模仿用户发起的 HTTP 请求。
它做的事,和你通过浏览器提交一个表单或访问某个网页是差不多的。
例如你传给它 url='xxx'、method='post' 等等参数就是告诉它:向 xxx 发起一个 http 请求

(3)关于 @copy_current_request_context

这是官方宣称在 1.0 中实现的一个新特性,http://flask.pocoo.org/docs/api/#flask.copy_current_request_context 看说明应该可以更加优雅的解决上述问题,

但是试了下貌似不行,可能是组件间的兼容性问题。

(4)关于 Streaming with Context

New in version 0.9.
Note that when you stream data, the request context is already gone the moment the function executes. Flask 0.9 provides you with a helper that can keep the request context around during the execution of the generator:

from flask import stream_with_context, request, Response

@app.route('/stream')
def streamed_response():
    def generate():
        yield 'Hello '
        yield request.args['name']
        yield '!'
    return Response(stream_with_context(generate()))

Without the stream_with_context() function you would get a RuntimeError at that point.

REF:

http://stackoverflow.com/questions/19755557/streaming-data-with-python-and-flask-raise-runtimeerror-working-outside-of-requ/20189866?noredirect=1#20189866

3、结论

(1)flask.request 和 streaming templates 兼容性不是很好,应该尽量不在 streaming templates 里调用 request,
把需要的值提前准备好,然后再传到 templates 里。这里也有人遇到同样的问题:

http://flask.pocoo.org/mailinglist/archive/2012/4/1/jinja2-stream-doesn-t-work/#8afda9ecd9682b16e8198a2f34e336fb

用 copy_current_request_context 没有效果应该也是上面这个原因。

(2)在文档语焉不详,同时 google 不到答案的时候,读源码或许是最后的选择,这也是一种能力吧。。。 - _ -

4、Refer:

http://stackoverflow.com/questions/13386681/streaming-data-with-python-and-flask
http://flask.pocoo.org/docs/patterns/streaming/

http://stackoverflow.com/questions/8224333/scrolling-log-file-tail-f-animation-using-javascript
http://jsfiddle.net/manuel/zejCD/1/

附坚实同学的 github 与 sf 地址:

https://github.com/anjianshi

http://segmentfault.com/u/anjianshi

5、最后附上完整的测试源码:

# -*- coding: utf-8 -*-
import sys

reload(sys)
sys.setdefaultencoding('utf-8')
from flask import Flask, request, Response

app = Flask(__name__)

def stream_template(template_name, **context):
    # http://flask.pocoo.org/docs/patterns/streaming/#streaming-from-templates
    app.update_template_context(context)
    t = app.jinja_env.get_template(template_name)
    rv = t.stream(context)
    # uncomment if you don't need immediate reaction
    ##rv.enable_buffering(5)
    return rv


@app.route('/')
@app.route('/demo', methods=['POST'])
def index():
    with app.test_request_context('/demo', method='POST', data=request.form) as new_context:
        def generateFunc():
            new_request = new_context.request
            if new_request.method == 'POST':
                if new_request.form['digitValue']:
                    num = int(new_request.form['digitValue'])
                    i = 0
                    for n in xrange(num):
                        i += 1
                        print "%s:\t%s" % (i, n)
                        yield i, n

        return Response(stream_template('index.html', data=generateFunc()))

if __name__ == "__main__":
    app.run(host='localhost', port=8888, debug=True)


<!DOCTYPE html>
<html>
<head>
    <title>Bootstrap 101 Template</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- Bootstrap -->
    <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
    <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
    <script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
    <![endif]-->
</head>
<body>

<style>
    #data {
        border: 1px solid blue;
        height: 500px;
        width: 500px;
        overflow: hidden;
    }
</style>
<script src="http://code.jquery.com/jquery-latest.js"></script>

<script>
    function tailScroll() {
        var height = $("#data").get(0).scrollHeight;
        $("#data").animate({
            scrollTop: height
        }, 5);
    }
</script>

<form role="form" action="/demo" method="POST">
    <textarea class="form-control" rows="1" name="digitValue"></textarea>
    <button type="submit" class="btn btn-default">Submit</button>
</form>

<div id="data" style="position:relative;height:400px; overflow-x:auto;overflow-y:auto">nothing received yet</div>


{% for i, resultStr in data: %}
    <script>
        $("<div />").text("{{ i }}:\t{{ resultStr }}").appendTo("#data")
        tailScroll();
    </script>
{% endfor %}

<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="https://code.jquery.com/jquery.js"></script>
<!-- Include all compiled plugins (below), or include individual files as needed -->
<script src="/static/dist/js/bootstrap.min.js"></script>
</body>
</html>

6、推荐阅读:

[1] 用Flask实现视频数据流传输

http://python.jobbole.com/80994/

https://github.com/miguelgrinberg/flask-video-streaming

[2] Video Streaming with Flask

http://blog.miguelgrinberg.com/post/video-streaming-with-flask

[3] Flask 的 Context 机制

https://blog.tonyseek.com/post/the-context-mechanism-of-flask/

你可能感兴趣的:(flask 流式响应 RuntimeError: working outside of request context)