Request 简介#

按照惯例,Request 作为参数被命名为 request,但你可以随意命名。参数的名称并不重要。以下两个处理程序都是有效的。

@app.get("/foo")
async def typical_use_case(request):
    return text("I said foo!")
@app.get("/foo")
async def atypical_use_case(req):
    return text("I said foo!")

注解请求对象非常简单。

from sanic.request import Request
from sanic.response import text

@app.get("/typed")
async def typed_handler(request: Request):
    return text("Done.")

请求体#

Request 对象允许你以几种不同的方式访问请求体的内容。

JSON 对象
$ curl localhost:8000 -d '{"foo": "bar"}'
>>> print(request.json)
{'foo': 'bar'}
raw 字节
$ curl localhost:8000 -d '{"foo": "bar"}'
>>> print(request.body)
b'{"foo": "bar"}'

小技巧

request.form 对象是几种类型中的一种,它是一个字典,每个值都是一个列表。这是因为 HTTP 允许使用单个键来发送多个值。

大多数情况下,你会想要使用 get() 方法来访问第一个元素而不是一个列表。如果你确实需要所有项目的列表,你可以使用 getlist()

$ curl localhost:8000 -d 'foo=bar'
>>> print(request.body)
b'foo=bar'

>>> print(request.form)
{'foo': ['bar']}

>>> print(request.form.get("foo"))
bar

>>> print(request.form.getlist("foo"))
['bar']

小技巧

request.files 对象是几种类型中的一种,它是一个字典,每个值都是一个列表。这是因为 HTTP 允许使用单个键来发送多个值。

大多数情况下,你会想要使用 get() 方法来访问第一个元素而不是一个列表。如果你确实需要所有项目的列表,你可以使用 getlist()

$ curl -F 'my_file=@/path/to/TEST' http://lo
>>> print(request.body)
b'--------------------------cb566ad845ad02d3\r\nContent-Disposition: form-data; name="my_file"; filename="TEST"\r\nContent-Type: application/octet-stream\r\n\r\nhello\n\r\n--------------------------cb566ad845ad02d3--\r\n'

>>> print(request.files)
{'my_file': [File(type='application/octet-stream', body=b'hello\n', name='TEST')]}

>>> print(request.files.get("my_file"))
File(type='application/octet-stream', body=b'hello\n', name='TEST')

>>> print(request.files.getlist("my_file"))
[File(type='application/octet-stream', body=b'hello\n', name='TEST')]
Context

请求上下文#

request.ctx#

request.ctx 对象是你的游乐场,用于存储你需要的关于请求的任何信息。它只存在于请求的持续时间内,并且对请求是唯一的。

这与 app.ctx 对象形成对比,后者在所有请求之间共享。小心不要将它们混淆!

默认情况下,request.ctx 对象是 SimpleNamespace 对象,允许你在其上设置任意属性。Sanic 不会将此对象用于任何其他目的,因此你可以自由地使用它,而不必担心名称冲突。

这通常用于存储诸如已验证用户详细信息之类的项目:

@app.on_request
async def run_before_handler(request):
    request.ctx.user = await fetch_user_by_token(request.token)

@app.route('/hi')
async def hi_my_name_is(request):
    if not request.ctx.user:
        return text("Hmm... I don't know you")
    return text(f"Hi, my name is {request.ctx.user.name}")

如你所见,request.ctx 对象是存储你需要在多个处理程序中访问的信息的好地方,这使得你的代码更 DRY(不重复)且更易于维护。但是,正如我们在后面中间件部分将要学到的,你还可以使用它来存储中间件中的信息,以便在另一个中间件中使用。

连接请求上下文#

通常你的 API 需要向同一个客户端提供多个并发(或连续的)请求。例如,这在需要查询多个端点以获取数据的进步型 Web 应用程序中非常常见。

HTTP 协议通过使用 keep alive 头来减轻由连接引起的开销时间。

@app.on_request
async def increment_foo(request):
    if not hasattr(request.conn_info.ctx, "foo"):
        request.conn_info.ctx.foo = 0
    request.conn_info.ctx.foo += 1

@app.get("/")
async def count_foo(request):
    return text(f"request.conn_info.ctx.foo={request.conn_info.ctx.foo}")
$ curl localhost:8000 localhost:8000 localhost:8000
request.conn_info.ctx.foo=1
request.conn_info.ctx.foo=2
request.conn_info.ctx.foo=3

警告

虽然这看起来像是一个方便的地方来存储单个 HTTP 连接的请求之间的信息,但不要假设单个连接上的所有请求都来自单个最终用户。这是因为 HTTP 代理和负载均衡器可以将多个连接复用到服务器的单个连接中。

不要使用这个来存储关于单个用户的信息。对于那个,请使用 request.ctx 对象。

请求的参数#

@app.route('/tag/<tag>')
async def tag_handler(request, tag):
    return text("Tag - {}".format(tag))

# or, explicitly as keyword arguments
@app.route('/tag/<tag>')
async def tag_handler(request, *, tag):
    return text("Tag - {}".format(tag))

在请求实例上有两个属性来获取查询参数: request.argsrequest.query_args。这允许您从请求路径(?后面的部分)访问查询参数。

在大多数用例中,您都希望使用请求对象来访问查询参数。这将是解析后的查询字符串作为字典。

这是目前为止最常见的模式。

考虑这个例子,我们有一个带有 q 参数的 /search 端点,我们想用它来搜索一些东西。

@app.get("/search")
async def search(request):
   query = request.args.get("q")
    if not query:
        return text("No query string provided")
    return text(f"Searching for: {query}")

有时,你可能希望以原始字符串或元组列表的形式访问查询字符串。为此,你可以使用 request.query_stringrequest.query_args 属性。

还应该注意的是,HTTP 允许单个键有多个值。尽管 request.args 似乎像是一个普通的字典,但它实际上是一个特殊类型,允许单个键有多个值。你可以通过使用 request.args.getlist() 方法来访问这个。

  • request.query_string - 原始查询字符串

  • request.query_args - 解析后的查询字符串作为元组列表

  • request.args - 解析后的查询字符串作为特殊字典

  • request.args.get() - 获取键的第一个值(类似于普通字典)

  • request.args.getlist() - 获取键的所有值

curl "http://localhost:8000?key1=val1&key2=val2&key1=val3"
>>> print(request.args)
{'key1': ['val1', 'val3'], 'key2': ['val2']}

>>> print(request.args.get("key1"))
val1

>>> print(request.args.getlist("key1"))
['val1', 'val3']

>>> print(request.query_args)
[('key1', 'val1'), ('key2', 'val2'), ('key1', 'val3')]

>>> print(request.query_string)
key1=val1&key2=val2&key1=val3

当前请求 getter#

有时,您可能会发现需要在应用程序中无法访问的位置访问当前请求。一个典型的例子可能是日志格式。您可以使用 request.get_current 来获取当前请求(如果有的话)。

请记住,请求对象仅限于单个 asyncio。正在运行处理程序的任务。如果您不在该任务中,则没有请求对象。

import logging

from sanic import Request, Sanic, json
from sanic.exceptions import SanicException
from sanic.log import LOGGING_CONFIG_DEFAULTS

LOGGING_FORMAT = (
    "%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: "
    "%(request_id)s %(request)s %(message)s %(status)d %(byte)d"
)

old_factory = logging.getLogRecordFactory()

def record_factory(*args, **kwargs):
    record = old_factory(*args, **kwargs)
    record.request_id = ""

    try:
        request = Request.get_current()
    except SanicException:
        ...
    else:
        record.request_id = str(request.id)

    return record

logging.setLogRecordFactory(record_factory)


LOGGING_CONFIG_DEFAULTS["formatters"]["access"]["format"] = LOGGING_FORMAT
app = Sanic("Example", log_config=LOGGING_CONFIG_DEFAULTS)

在这个例子中,我们将 request.id 添加到每个访问日志消息中。