请求对象
一个请求从客户端发出,它大致经过了这些变化:从HTTP请求报文,到符合WSGI规定的Python字典(environ),再到 Werkzeug
中的werkzeug.wrappers.Request
对象,最后再到 Flask
中的请求对象 request
。
从 Flask 中导入的 request 是代理,被代理的实际对象是 RequestContext
对象的 request 属性,这个属性存储的是 Request
类实例,这个Request才是表示请求的请求对象
flask/wrappers.py:Request 源码:
class JSONMixin(_JSONMixin):
json_module = json
def on_json_loading_failed(self, e):
if current_app and current_app.debug:
raise BadRequest("Failed to decode JSON object: {0}".format(e))
raise BadRequest()
class Request(RequestBase, JSONMixin):
url_rule = None
view_args = None
routing_exception = None
@property
def max_content_length(self):
# 返回配置的 MAX_CONTENT_LENGTH 的值
if current_app:
return current_app.config["MAX_CONTENT_LENGTH"]
@property
def endpoint(self):
# 返回与请求匹配的 endpoint
if self.url_rule is not None:
return self.url_rule.endpoint
@property
def blueprint(self):
# 当前蓝图的名称
if self.url_rule and "." in self.url_rule.endpoint:
return self.url_rule.endpoint.rsplit(".", 1)[0]
def _load_form_data(self):
RequestBase._load_form_data(self)
# In debug mode we're replacing the files multidict with an ad-hoc
# subclass that raises a different error for key errors.
if (
current_app
and current_app.debug
and self.mimetype != "multipart/form-data"
and not self.files
):
from .debughelpers import attach_enctype_error_multidict
attach_enctype_error_multidict(self)
Request 类继承 werkzeug.wrappers:Request
类和添加JSON支持的 JSONMixin
类,并添加了一些和flask 相关的属性,比如 view_args、blueprint、json 处理等
werkzeug.wrappers:Request 源码:
class Request(
BaseRequest,
AcceptMixin,
ETagRequestMixin,
UserAgentMixin,
AuthorizationMixin,
CORSRequestMixin,
CommonRequestDescriptorsMixin,
):
"""Full featured request object implementing the following mixins:
- :class:`AcceptMixin` for accept header parsing
- :class:`ETagRequestMixin` for etag and cache control handling
- :class:`UserAgentMixin` for user agent introspection
- :class:`AuthorizationMixin` for http auth handling
- :class:`~werkzeug.wrappers.cors.CORSRequestMixin` for Cross
Origin Resource Sharing headers
- :class:`CommonRequestDescriptorsMixin` for common headers
"""
它没有任何的 body。但是有多个基类,第一个是 BaseRequest
,其他的都是各种 Mixin
。
这里要讲一下 Mixin 机制,这是 python 多继承的一种方式,如果你希望某个类可以自行组合它的特性(比如这里的情况),或者希望某个特性用在多个类中,就可以使用 Mixin。
如果我们只需要能处理各种 Accept
头部的请求,可以这样做:
class Request(BaseRequest, AcceptMixin):
pass
我们先来看看 BaseRequest
:
class BaseRequest(object):
def __init__(self, environ, populate_request=True, shallow=False):
self.environ = environ
if populate_request and not shallow:
self.environ['werkzeug.request'] = self
self.shallow = shallow
...
@cached_property
def headers(self):
"""The headers from the WSGI environ as immutable
:class:`~werkzeug.datastructures.EnvironHeaders`.
"""
return EnvironHeaders(self.environ)
@cached_property
def path(self):
"""Requested path as unicode. This works a bit like the regular path
info in the WSGI environment but will always include a leading slash,
even if the URL root is accessed.
"""
raw_path = wsgi_decoding_dance(
self.environ.get("PATH_INFO") or "", self.charset, self.encoding_errors
)
return "/" + raw_path.lstrip("/")
实例化需要的唯一变量是 environ
,它只是简单地把变量保存下来,并没有做进一步的处理。
关于 Request
内部各种属性的实现,就不分析了,因为它们每个具体的实现都不太一样,也不复杂,无外乎对 environ
字典中某些字段做一些处理和计算
接下我们再看看 Mixin,这里只用 AcceptMixin
作为例子:
class AcceptMixin(object):
@cached_property
def accept_mimetypes(self):
# 客户端支持的mimetype列表
return parse_accept_header(self.environ.get("HTTP_ACCEPT"), MIMEAccept)
@cached_property
def accept_charsets(self):
# 客户端支持的字符集列表
return parse_accept_header(
self.environ.get("HTTP_ACCEPT_CHARSET"), CharsetAccept
)
@cached_property
def accept_encodings(self):
# 客户端接受的编码列表
return parse_accept_header(self.environ.get("HTTP_ACCEPT_ENCODING"))
@cached_property
def accept_languages(self):
# 客户接受的语言列表
return parse_accept_header(
self.environ.get("HTTP_ACCEPT_LANGUAGE"), LanguageAccept
)
AcceptMixin
实现了请求内容协商的部分,比如请求接受的语言、编码格式、相应内容等。
虽然自己没有 __init__
方法,但是也直接使用了self.environ
,因此它并不能直接使用,只能和 BaseRequest
一起出现。
响应对象
我们知道响应是由 finalize_request()
方法生成的,它调用了 flask.Flask.make_response()
方法生成响应对象,传入的 rv 参数是 dispatch_request() 的返回值,也就是视图函数的返回值。
HTTP 响应分为三个部分:状态栏(HTTP 版本、状态码和说明)、头部(以冒号隔开的字符对,用于各种控制和协商)、body(服务端返回的数据)
视图函数可以返回多种类型的返回值:
finalize_request
的代码如下:
def finalize_request(self, rv, from_error_handler=False):
response = self.make_response(rv)
try:
response = self.process_response(response)
request_finished.send(self, response=response)
except Exception:
if not from_error_handler:
raise
self.logger.exception(
"Request finalizing failed with an error while handling an error"
)
return response
make_response
根据视图函数的返回值生成 response 对象,process_response
对 response 做一些后续的处理(比如执行 hooks 函数)
我们先来看看 make_response
:
def make_response(self, rv):
status = headers = None
# unpack tuple returns
if isinstance(rv, tuple):
len_rv = len(rv)
# a 3-tuple is unpacked directly
if len_rv == 3:
rv, status, headers = rv
# decide if a 2-tuple has status or headers
elif len_rv == 2:
if isinstance(rv[1], (Headers, dict, tuple, list)):
rv, headers = rv
else:
rv, status = rv
# other sized tuples are not allowed
else:
raise TypeError(
"The view function did not return a valid response tuple."
" The tuple must have the form (body, status, headers),"
" (body, status), or (body, headers)."
)
# the body must not be None
if rv is None:
raise TypeError(
"The view function did not return a valid response. The"
" function either returned None or ended without a return"
" statement."
)
# make sure the body is an instance of the response class
if not isinstance(rv, self.response_class):
if isinstance(rv, (text_type, bytes, bytearray)):
# let the response class set the status and headers instead of
# waiting to do it manually, so that the class can handle any
# special logic
rv = self.response_class(rv, status=status, headers=headers)
status = headers = None
elif isinstance(rv, dict):
rv = jsonify(rv)
elif isinstance(rv, BaseResponse) or callable(rv):
# evaluate a WSGI callable, or coerce a different response
# class to the correct type
try:
rv = self.response_class.force_type(rv, request.environ)
except TypeError as e:
new_error = TypeError(
"{e}\nThe 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"
" {rv.__class__.__name__}.".format(e=e, rv=rv)
)
reraise(TypeError, new_error, sys.exc_info()[2])
else:
raise 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"
" {rv.__class__.__name__}.".format(rv=rv)
)
# prefer the status if it was provided
if status is not None:
if isinstance(status, (text_type, bytes, bytearray)):
rv.status = status
else:
rv.status_code = status
# extend existing headers with provided headers
if headers:
rv.headers.extend(headers)
return rv
make_response
是视图函数能返回多个不同数量和类型值的关键,因为它能处理这些情况,统一把它们转换成 response。
如果返回值本身就是 Response 实例,就直接使用它;如果返回值是字符串类型,就把它作为响应的 body,并自动设置状态码和头部信息;如果返回值是 tuple,会尝试用 (response, status, headers) 或者 (response, headers) 去解析。
注意:因为视图函数可以返回
Response
对象,因此我们可以直接操作Response
。
flask/wrappers.py:Response 源码:
class Response(ResponseBase, JSONMixin):
default_mimetype = "text/html"
def _get_data_for_json(self, cache):
return self.get_data()
@property
def max_cookie_size(self):
# 返回配置的 MAX_COOKIE_SIZE 值
if current_app:
return current_app.config["MAX_COOKIE_SIZE"]
# return Werkzeug's default when not in an app context
return super(Response, self).max_cookie_size
Flask 的 Response
类非常简单,它只是继承了 werkzeug.wrappers:Response
,然后设置默认返回类型为 html。
不过从注释中,我们得到两条很有用的信息:
- 一般情况下不要直接操作
Response
对象,而是使用make_response
方法来生成它 - 如果需要使用自定义的响应对象,可以覆盖 flask app 对象的
response_class
属性。
werkzeug 实现的 response 定义在 werkzeug/wrappers.py
文件中:
class Response(
BaseResponse,
ETagResponseMixin,
WWWAuthenticateMixin,
CORSResponseMixin,
ResponseStreamMixin,
CommonResponseDescriptorsMixin,
):
"""Full featured response object implementing the following mixins:
- :class:`ETagResponseMixin` for etag and cache control handling
- :class:`WWWAuthenticateMixin` for HTTP authentication support
- :class:`~werkzeug.wrappers.cors.CORSResponseMixin` for Cross
Origin Resource Sharing headers
- :class:`ResponseStreamMixin` to add support for the ``stream``
property
- :class:`CommonResponseDescriptorsMixin` for various HTTP
descriptors
"""
和我们分析的 Request 类一样,这里使用了 Mixin 机制。BaseResponse
精简后的大概框架如下:
class BaseResponse(object):
"""Base response class. The most important fact about a response object
is that it's a regular WSGI application. It's initialized with a couple
of response parameters (headers, body, status code etc.) and will start a
valid WSGI response when called with the environ and start response
callable.
"""
charset = 'utf-8'
default_status = 200
default_mimetype = 'text/plain'
automatically_set_content_length = True
def __init__(self, response=None, status=None, headers=None,
mimetype=None, content_type=None, direct_passthrough=False):
pass
BaseResponse
有一些类属性,定义了默认的值,比如默认字符编码是 utf-8,默认状态码是 200 等。
实例化的时候接受的参数有:
- response: 字符串或者其他 iterable 对象,作为响应的 body
- status: 状态码,可以是整数,也可以是字符串
- headers: 响应的头部,可以是个列表,也可以是
werkzeug.datastructures.Headers
对象 - mimetype: mimetype 类型,告诉客户端响应 body 的格式,默认是文本格式
- content_type: 响应头部的
Content-Type
内容
所有这些参数都是可选的,默认情况下会生成一个状态码为 200,没有任何 body 的响应。status、status_code 作为 Response
的属性,可以直接读取和修改。
对外也提供了直接读写的接口 self.data
:
def get_data(self, as_text=False):
"""The string representation of the request body. Whenever you call
this property the request iterable is encoded and flattened.
"""
self._ensure_sequence()
rv = b''.join(self.iter_encoded())
if as_text:
rv = rv.decode(self.charset)
return rv
def set_data(self, value):
"""Sets a new string as response. The value set must either by a
unicode or bytestring.
"""
if isinstance(value, text_type):
value = value.encode(self.charset)
else:
value = bytes(value)
self.response = [value]
if self.automatically_set_content_length:
self.headers['Content-Length'] = str(len(value))
data = property(get_data, set_data, doc='''
A descriptor that calls :meth:`get_data` and :meth:`set_data`. This
should not be used and will eventually get deprecated.
''')
body 字符的编码和长度都是自动设置的,用户不需要手动处理。