请求上下文

在请求当中,请求上下文保持对请求层级的数据的追踪。相比在请求当中把请求对象传递到每个函数当中,Flask 使用了 requestsession 代理对象来访问请求对象。

This is similar to 应用上下文, which keeps track of the application-level data independent of a request. A corresponding application context is pushed when a request context is pushed.

上下文的目的

Flask 应用处理请求,根据从 WSGI 服务器获取到的环境,创建出相应的 Request 对象。因为 工作者 (根据不同的服务器可能为线程,进程或协程)一次只能处理一个请求,在请求当中,请求数据可以认为对工作者全局可见、Flask 用术语 上下文局部变量(context local) 来表示这种设计。

Flask 在处理请求时,会自动 推入 请求上下文。视图函数,错误处理钩子函数与其他在请求当中运行的函数可以访问指向当前请求的请求对象代理对象 request

上下文的生命周期

When a Flask application begins handling a request, it pushes a request context, which also pushes an app context. When the request ends it pops the request context then the application context.

每个线程(或者其他工作者类型)的上下文是独立的。request 不能传入其他线程,其他线程有不同的上下文栈,因此不会知道父线程会指向哪个请求。

上下文局部变量的实现在 Werkzeug 中。若要了解更多内部工作细节,参见 Context Locals

手动推入上下文

如果要在应用上下文之外的地方试图去获取或者使用依赖 request 的任何东西,有可能会收到这样的错误信息:

RuntimeError: Working outside of request context.

This typically means that you attempted to use functionality that
needed an active HTTP request. Consult the documentation on testing
for information about how to avoid this problem.

这通常只会在测试需要活动请求的代码时候发生。其中一个方法是使用 test client 来模拟一个完整请求。或者可以在 with 块使用 test_request_context(),所有在块中运行的代码将会可以访问由测试数据生成的 request

def generate_report(year):
    format = request.args.get('format')
    ...

with app.test_request_context(
        '/make_report/2017', data={'format': 'short'}):
    generate_report()

如果你在测试应用以外场景遇到这个错误,绝大多数情况下意味着这些代码应该转移到视图函数下。

了解更多如何在 Python 交互式命令行中使用请求上下文的信息,参见 使用 Shell

上下文的运作原理

在处理每个请求时,都会调用 Flask.wsgi_app() 方法。它在请求中控制上下文。具体来说,存储请求上下文和应用上下文的数据结构为栈,分别为 _request_ctx_stack 以及 _app_ctx_stack。当上下文被推入栈,依赖栈的代理对象变得可用,指向在栈顶的上下文。

当请求开始,将创建并推入 RequestContext 对象。如果此时应用上下文不在上下文栈的顶部,将先创建 AppContext。当这些上下文被推入,代理对象 current_appgrequest 以及 session 在处理请求的线程变得可用。

因为上下文对象存放在栈中,在请求中,其他上下文对象推入时可能会改变代理对象的指向。尽管这不是常见的设计模式,但它可以让应用能够内部重定向或者将不同应用串联在一起。

当请求被分配到视图函数,生成并发送响应,请求上下文先被弹出,然后应用上下文也被弹出。在上下文被弹出前,函数 teardown_request() 以及 teardown_appcontext() 会被执行。即使在请求被分配过程中有未处理的请求抛出,这些函数也会被执行。

回调与错误

Flask 在不同阶段中会分配请求,这会影响请求、响应以及错误处理。在这些阶段中,上下文处于可用状态。

Blueprint 可以为这些事件添加蓝图专属的钩子函数。当蓝图所属的路由匹配上请求,那么蓝图内的钩子函数将被执行。

  1. 在每个请求前,会调用 before_request() 函数。如果其中的一个函数返回了值,那么其他函数会被跳过执行。返回值会当作响应,视图函数不会被调用。

  2. 如果 before_request() 函数不返回响应,被路由匹配的视图函数将被调用,返回请求。

  3. 视图函数返回的值会被转换成实际的响应对象,然后传递到 after_request() 函数。每个函数返回一个被修改的或者是新的响应对象。

  4. 当响应被返回,在弹出上下文的过程中,会调用 teardown_request()teardown_appcontext() 函数。即使内部有一个未处理的异常抛出,这些函数也会被执行。

如果异常在清理(teardown)函数中抛出,Flask 会尝试使用 errorhandler() 函数去处理异常,返回响应。如果没有错误钩子函数,或者钩子函数本身抛出了异常,Flask 返回了通用的 500 Internal Server Error 响应。清理函数一样会调用,而且会传入异常对象。

在 debug 模式开启的时候,未处理的异常不会被转换为 500 响应,而是传递给 WSGI 服务器。这让开发服务器可以展示带有异常跟踪(trackback)的交互式调试器。

清理钩子函数

清理钩子函数独立于请求分配,而是在上下文被弹出的时候才调用。当在请求分配的过程中,或者在手动推入的的上下文中有未捕抓的异常,这些函数依然会被调用。这意味着不会保证在请求分配的每一部分都会首先执行。确保在编写这些函数的时候不要依赖其他钩子函数,不要假设函数不会失败。

在测试的过程中,在请求结束的时候推迟弹出上下文是一种很有用的手段,它可以让测试函数访问上下文中的数据。在 with 块中使用 test_client() 来让上下文在离开 with 块前保留。

from flask import Flask, request

app = Flask(__name__)

@app.route('/')
def hello():
    print('during view')
    return 'Hello, World!'

@app.teardown_request
def show_teardown(exception):
    print('after with block')

with app.test_request_context():
    print('during with block')

# teardown functions are called after the context with block exits

with app.test_client() as client:
    client.get('/')
    # the contexts are not popped even though the request ended
    print(request.path)

# the contexts are popped and teardown functions are called after
# the client with block exits

信号

如果 signals_available 为真,下列信号将被发送:

  1. before_request() 被调用前,request_started 信号会被发送。

  2. after_request() 被调用前,request_finished 信号会被发送。

  3. 当异常开始处理时,got_request_exception 信号会在 errorhandler() 被查看或调用前被发送。

  4. teardown_request() 被调用后,request_tearing_down 信号会被发送。

在错误时保留上下文

在请求结束时,上下文会被弹出,所有与上下文相关的数据会被销毁。如果在开发环境中有错误发生,推迟销毁上下文数据对调试来说非常有用。

当开发服务器在开发模式运行(FLASK_ENV 环境变量被设置为 'development'),错误与数据会被保留,在交互式调试器中展示。

这一行为能被 PRESERVE_CONTEXT_ON_EXCEPTION 设置项所控制。如上文所述。在开发环境中默认设置为 True

不要在生产环境中开启 PRESERVE_CONTEXT_ON_EXCEPTION,因为这会让应用在出现异常时内存泄露。

代理对象的注意事项

一些 Flask 提供的对象,是其他对象的代理。在每个工作线程中,代理对象会以相同的方式被访问。在内部实现中,代理对象指向绑定到工作者的唯一的对象。细节如本页所述。

大多数时候,这些细节无需担心。但有些时候最好还是知道这个对象实际上是一个代理:

  • 代理对象不能冒充实际指向的对象类型。如果要进行实例检查,应当要在被代理的对象本身进行检查。

  • 在某些要使用被代理对象的引用的时候,如发送 Signals 或者向后台线程传递数据。

如果需要访问在底层被代理的对象,使用 _get_current_object() 这个方法:

app = current_app._get_current_object()
my_signal.send(app)