请求对象

一个请求从客户端发出,它大致经过了这些变化:从HTTP请求报文,到符合WSGI规定的Python字典(environ),再到 Werkzeug 中的werkzeug.wrappers.Request 对象,最后再到 Flask 中的请求对象 request

从 Flask 中导入的 request 是代理,被代理的实际对象是 RequestContext 对象的 request 属性,这个属性存储的是 Request 类实例,这个Request才是表示请求的请求对象

flask/wrappers.py:Request 源码:

  1. class JSONMixin(_JSONMixin):
  2. json_module = json
  3. def on_json_loading_failed(self, e):
  4. if current_app and current_app.debug:
  5. raise BadRequest("Failed to decode JSON object: {0}".format(e))
  6. raise BadRequest()
  7. class Request(RequestBase, JSONMixin):
  8. url_rule = None
  9. view_args = None
  10. routing_exception = None
  11. @property
  12. def max_content_length(self):
  13. # 返回配置的 MAX_CONTENT_LENGTH 的值
  14. if current_app:
  15. return current_app.config["MAX_CONTENT_LENGTH"]
  16. @property
  17. def endpoint(self):
  18. # 返回与请求匹配的 endpoint
  19. if self.url_rule is not None:
  20. return self.url_rule.endpoint
  21. @property
  22. def blueprint(self):
  23. # 当前蓝图的名称
  24. if self.url_rule and "." in self.url_rule.endpoint:
  25. return self.url_rule.endpoint.rsplit(".", 1)[0]
  26. def _load_form_data(self):
  27. RequestBase._load_form_data(self)
  28. # In debug mode we're replacing the files multidict with an ad-hoc
  29. # subclass that raises a different error for key errors.
  30. if (
  31. current_app
  32. and current_app.debug
  33. and self.mimetype != "multipart/form-data"
  34. and not self.files
  35. ):
  36. from .debughelpers import attach_enctype_error_multidict
  37. attach_enctype_error_multidict(self)

Request 类继承 werkzeug.wrappers:Request 类和添加JSON支持的 JSONMixin 类,并添加了一些和flask 相关的属性,比如 view_args、blueprint、json 处理等

werkzeug.wrappers:Request 源码:

  1. class Request(
  2. BaseRequest,
  3. AcceptMixin,
  4. ETagRequestMixin,
  5. UserAgentMixin,
  6. AuthorizationMixin,
  7. CORSRequestMixin,
  8. CommonRequestDescriptorsMixin,
  9. ):
  10. """Full featured request object implementing the following mixins:
  11. - :class:`AcceptMixin` for accept header parsing
  12. - :class:`ETagRequestMixin` for etag and cache control handling
  13. - :class:`UserAgentMixin` for user agent introspection
  14. - :class:`AuthorizationMixin` for http auth handling
  15. - :class:`~werkzeug.wrappers.cors.CORSRequestMixin` for Cross
  16. Origin Resource Sharing headers
  17. - :class:`CommonRequestDescriptorsMixin` for common headers
  18. """

它没有任何的 body。但是有多个基类,第一个是 BaseRequest,其他的都是各种 Mixin
这里要讲一下 Mixin 机制,这是 python 多继承的一种方式,如果你希望某个类可以自行组合它的特性(比如这里的情况),或者希望某个特性用在多个类中,就可以使用 Mixin。

如果我们只需要能处理各种 Accept 头部的请求,可以这样做:

  1. class Request(BaseRequest, AcceptMixin):
  2. pass

我们先来看看 BaseRequest:

  1. class BaseRequest(object):
  2. def __init__(self, environ, populate_request=True, shallow=False):
  3. self.environ = environ
  4. if populate_request and not shallow:
  5. self.environ['werkzeug.request'] = self
  6. self.shallow = shallow
  7. ...
  8. @cached_property
  9. def headers(self):
  10. """The headers from the WSGI environ as immutable
  11. :class:`~werkzeug.datastructures.EnvironHeaders`.
  12. """
  13. return EnvironHeaders(self.environ)
  14. @cached_property
  15. def path(self):
  16. """Requested path as unicode. This works a bit like the regular path
  17. info in the WSGI environment but will always include a leading slash,
  18. even if the URL root is accessed.
  19. """
  20. raw_path = wsgi_decoding_dance(
  21. self.environ.get("PATH_INFO") or "", self.charset, self.encoding_errors
  22. )
  23. return "/" + raw_path.lstrip("/")

实例化需要的唯一变量是 environ,它只是简单地把变量保存下来,并没有做进一步的处理。

关于 Request 内部各种属性的实现,就不分析了,因为它们每个具体的实现都不太一样,也不复杂,无外乎对 environ 字典中某些字段做一些处理和计算

接下我们再看看 Mixin,这里只用 AcceptMixin 作为例子:

  1. class AcceptMixin(object):
  2. @cached_property
  3. def accept_mimetypes(self):
  4. # 客户端支持的mimetype列表
  5. return parse_accept_header(self.environ.get("HTTP_ACCEPT"), MIMEAccept)
  6. @cached_property
  7. def accept_charsets(self):
  8. # 客户端支持的字符集列表
  9. return parse_accept_header(
  10. self.environ.get("HTTP_ACCEPT_CHARSET"), CharsetAccept
  11. )
  12. @cached_property
  13. def accept_encodings(self):
  14. # 客户端接受的编码列表
  15. return parse_accept_header(self.environ.get("HTTP_ACCEPT_ENCODING"))
  16. @cached_property
  17. def accept_languages(self):
  18. # 客户接受的语言列表
  19. return parse_accept_header(
  20. self.environ.get("HTTP_ACCEPT_LANGUAGE"), LanguageAccept
  21. )

AcceptMixin 实现了请求内容协商的部分,比如请求接受的语言、编码格式、相应内容等。
虽然自己没有 __init__ 方法,但是也直接使用了
self.environ,因此它并不能直接使用,只能和 BaseRequest 一起出现。

响应对象

我们知道响应是由 finalize_request() 方法生成的,它调用了 flask.Flask.make_response() 方法生成响应对象,传入的 rv 参数是 dispatch_request() 的返回值,也就是视图函数的返回值。

HTTP 响应分为三个部分:状态栏(HTTP 版本、状态码和说明)、头部(以冒号隔开的字符对,用于各种控制和协商)、body(服务端返回的数据)

视图函数可以返回多种类型的返回值:
image.png

finalize_request 的代码如下:

  1. def finalize_request(self, rv, from_error_handler=False):
  2. response = self.make_response(rv)
  3. try:
  4. response = self.process_response(response)
  5. request_finished.send(self, response=response)
  6. except Exception:
  7. if not from_error_handler:
  8. raise
  9. self.logger.exception(
  10. "Request finalizing failed with an error while handling an error"
  11. )
  12. return response

make_response 根据视图函数的返回值生成 response 对象,process_response 对 response 做一些后续的处理(比如执行 hooks 函数)

我们先来看看 make_response

  1. def make_response(self, rv):
  2. status = headers = None
  3. # unpack tuple returns
  4. if isinstance(rv, tuple):
  5. len_rv = len(rv)
  6. # a 3-tuple is unpacked directly
  7. if len_rv == 3:
  8. rv, status, headers = rv
  9. # decide if a 2-tuple has status or headers
  10. elif len_rv == 2:
  11. if isinstance(rv[1], (Headers, dict, tuple, list)):
  12. rv, headers = rv
  13. else:
  14. rv, status = rv
  15. # other sized tuples are not allowed
  16. else:
  17. raise TypeError(
  18. "The view function did not return a valid response tuple."
  19. " The tuple must have the form (body, status, headers),"
  20. " (body, status), or (body, headers)."
  21. )
  22. # the body must not be None
  23. if rv is None:
  24. raise TypeError(
  25. "The view function did not return a valid response. The"
  26. " function either returned None or ended without a return"
  27. " statement."
  28. )
  29. # make sure the body is an instance of the response class
  30. if not isinstance(rv, self.response_class):
  31. if isinstance(rv, (text_type, bytes, bytearray)):
  32. # let the response class set the status and headers instead of
  33. # waiting to do it manually, so that the class can handle any
  34. # special logic
  35. rv = self.response_class(rv, status=status, headers=headers)
  36. status = headers = None
  37. elif isinstance(rv, dict):
  38. rv = jsonify(rv)
  39. elif isinstance(rv, BaseResponse) or callable(rv):
  40. # evaluate a WSGI callable, or coerce a different response
  41. # class to the correct type
  42. try:
  43. rv = self.response_class.force_type(rv, request.environ)
  44. except TypeError as e:
  45. new_error = TypeError(
  46. "{e}\nThe view function did not return a valid"
  47. " response. The return type must be a string, dict, tuple,"
  48. " Response instance, or WSGI callable, but it was a"
  49. " {rv.__class__.__name__}.".format(e=e, rv=rv)
  50. )
  51. reraise(TypeError, new_error, sys.exc_info()[2])
  52. else:
  53. raise TypeError(
  54. "The view function did not return a valid"
  55. " response. The return type must be a string, dict, tuple,"
  56. " Response instance, or WSGI callable, but it was a"
  57. " {rv.__class__.__name__}.".format(rv=rv)
  58. )
  59. # prefer the status if it was provided
  60. if status is not None:
  61. if isinstance(status, (text_type, bytes, bytearray)):
  62. rv.status = status
  63. else:
  64. rv.status_code = status
  65. # extend existing headers with provided headers
  66. if headers:
  67. rv.headers.extend(headers)
  68. return rv

make_response 是视图函数能返回多个不同数量和类型值的关键,因为它能处理这些情况,统一把它们转换成 response。

如果返回值本身就是 Response 实例,就直接使用它;如果返回值是字符串类型,就把它作为响应的 body,并自动设置状态码和头部信息;如果返回值是 tuple,会尝试用 (response, status, headers) 或者 (response, headers) 去解析。

注意:因为视图函数可以返回 Response 对象,因此我们可以直接操作 Response

flask/wrappers.py:Response 源码:

  1. class Response(ResponseBase, JSONMixin):
  2. default_mimetype = "text/html"
  3. def _get_data_for_json(self, cache):
  4. return self.get_data()
  5. @property
  6. def max_cookie_size(self):
  7. # 返回配置的 MAX_COOKIE_SIZE 值
  8. if current_app:
  9. return current_app.config["MAX_COOKIE_SIZE"]
  10. # return Werkzeug's default when not in an app context
  11. return super(Response, self).max_cookie_size

Flask 的 Response 类非常简单,它只是继承了 werkzeug.wrappers:Response,然后设置默认返回类型为 html。

不过从注释中,我们得到两条很有用的信息:

  1. 一般情况下不要直接操作 Response 对象,而是使用 make_response 方法来生成它
  2. 如果需要使用自定义的响应对象,可以覆盖 flask app 对象的 response_class 属性。

werkzeug 实现的 response 定义在 werkzeug/wrappers.py 文件中:

  1. class Response(
  2. BaseResponse,
  3. ETagResponseMixin,
  4. WWWAuthenticateMixin,
  5. CORSResponseMixin,
  6. ResponseStreamMixin,
  7. CommonResponseDescriptorsMixin,
  8. ):
  9. """Full featured response object implementing the following mixins:
  10. - :class:`ETagResponseMixin` for etag and cache control handling
  11. - :class:`WWWAuthenticateMixin` for HTTP authentication support
  12. - :class:`~werkzeug.wrappers.cors.CORSResponseMixin` for Cross
  13. Origin Resource Sharing headers
  14. - :class:`ResponseStreamMixin` to add support for the ``stream``
  15. property
  16. - :class:`CommonResponseDescriptorsMixin` for various HTTP
  17. descriptors
  18. """

和我们分析的 Request 类一样,这里使用了 Mixin 机制。BaseResponse 精简后的大概框架如下:

  1. class BaseResponse(object):
  2. """Base response class. The most important fact about a response object
  3. is that it's a regular WSGI application. It's initialized with a couple
  4. of response parameters (headers, body, status code etc.) and will start a
  5. valid WSGI response when called with the environ and start response
  6. callable.
  7. """
  8. charset = 'utf-8'
  9. default_status = 200
  10. default_mimetype = 'text/plain'
  11. automatically_set_content_length = True
  12. def __init__(self, response=None, status=None, headers=None,
  13. mimetype=None, content_type=None, direct_passthrough=False):
  14. 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

  1. def get_data(self, as_text=False):
  2. """The string representation of the request body. Whenever you call
  3. this property the request iterable is encoded and flattened.
  4. """
  5. self._ensure_sequence()
  6. rv = b''.join(self.iter_encoded())
  7. if as_text:
  8. rv = rv.decode(self.charset)
  9. return rv
  10. def set_data(self, value):
  11. """Sets a new string as response. The value set must either by a
  12. unicode or bytestring.
  13. """
  14. if isinstance(value, text_type):
  15. value = value.encode(self.charset)
  16. else:
  17. value = bytes(value)
  18. self.response = [value]
  19. if self.automatically_set_content_length:
  20. self.headers['Content-Length'] = str(len(value))
  21. data = property(get_data, set_data, doc='''
  22. A descriptor that calls :meth:`get_data` and :meth:`set_data`. This
  23. should not be used and will eventually get deprecated.
  24. ''')

body 字符的编码和长度都是自动设置的,用户不需要手动处理。