请求对象
一个请求从客户端发出,它大致经过了这些变化:从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 = jsondef 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 = Noneview_args = Nonerouting_exception = None@propertydef max_content_length(self):# 返回配置的 MAX_CONTENT_LENGTH 的值if current_app:return current_app.config["MAX_CONTENT_LENGTH"]@propertydef endpoint(self):# 返回与请求匹配的 endpointif self.url_rule is not None:return self.url_rule.endpoint@propertydef 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_appand current_app.debugand self.mimetype != "multipart/form-data"and not self.files):from .debughelpers import attach_enctype_error_multidictattach_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 CrossOrigin 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 = environif populate_request and not shallow:self.environ['werkzeug.request'] = selfself.shallow = shallow...@cached_propertydef headers(self):"""The headers from the WSGI environ as immutable:class:`~werkzeug.datastructures.EnvironHeaders`."""return EnvironHeaders(self.environ)@cached_propertydef path(self):"""Requested path as unicode. This works a bit like the regular pathinfo 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_propertydef accept_mimetypes(self):# 客户端支持的mimetype列表return parse_accept_header(self.environ.get("HTTP_ACCEPT"), MIMEAccept)@cached_propertydef accept_charsets(self):# 客户端支持的字符集列表return parse_accept_header(self.environ.get("HTTP_ACCEPT_CHARSET"), CharsetAccept)@cached_propertydef accept_encodings(self):# 客户端接受的编码列表return parse_accept_header(self.environ.get("HTTP_ACCEPT_ENCODING"))@cached_propertydef 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:raiseself.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 returnsif isinstance(rv, tuple):len_rv = len(rv)# a 3-tuple is unpacked directlyif len_rv == 3:rv, status, headers = rv# decide if a 2-tuple has status or headerselif len_rv == 2:if isinstance(rv[1], (Headers, dict, tuple, list)):rv, headers = rvelse:rv, status = rv# other sized tuples are not allowedelse: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 Noneif 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 classif 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 logicrv = self.response_class(rv, status=status, headers=headers)status = headers = Noneelif 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 typetry: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 providedif status is not None:if isinstance(status, (text_type, bytes, bytearray)):rv.status = statuselse:rv.status_code = status# extend existing headers with provided headersif 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()@propertydef 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 contextreturn 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 CrossOrigin Resource Sharing headers- :class:`ResponseStreamMixin` to add support for the ``stream``property- :class:`CommonResponseDescriptorsMixin` for various HTTPdescriptors"""
和我们分析的 Request 类一样,这里使用了 Mixin 机制。BaseResponse 精简后的大概框架如下:
class BaseResponse(object):"""Base response class. The most important fact about a response objectis that it's a regular WSGI application. It's initialized with a coupleof response parameters (headers, body, status code etc.) and will start avalid WSGI response when called with the environ and start responsecallable."""charset = 'utf-8'default_status = 200default_mimetype = 'text/plain'automatically_set_content_length = Truedef __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 callthis 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 rvdef set_data(self, value):"""Sets a new string as response. The value set must either by aunicode 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`. Thisshould not be used and will eventually get deprecated.''')
body 字符的编码和长度都是自动设置的,用户不需要手动处理。
