Request
See API docs: sanic.request
The sanic.request.Request
instance contains a lot of helpful information available on its parameters. Refer to the API documentation for full details.
As we saw in the section on handlers, the first argument in a route handler is usually the sanic.request.Request
object. Because Sanic is an async framework, the handler will run inside of a asyncio.Task
and will be scheduled by the event loop. This means that the handler will be executed in an isolated context and the request object will be unique to that handler's task.
By convention, the argument is named request
, but you can name it whatever you want. The name of the argument is not important. Both of the following handlers are valid.
@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!")
Annotating a request object is super simple.
from sanic.request import Request
from sanic.response import text
@app.get("/typed")
async def typed_handler(request: Request):
return text("Done.")
Tip
For your convenience, assuming you are using a modern IDE, you should leverage type annotations to help with code completion and documentation. This is especially helpful when using the request
object as it has MANY properties and methods.
To see the full list of available properties and methods, refer to the API documentation.
Body#
The Request
object allows you to access the content of the request body in a few different ways.
JSON#
Parameter: request.json
Description: The parsed JSON object
$ curl localhost:8000 -d '{"foo": "bar"}'
>>> print(request.json)
{'foo': 'bar'}
Raw#
Parameter: request.body
Description: The raw bytes from the request body
$ curl localhost:8000 -d '{"foo": "bar"}'
>>> print(request.body)
b'{"foo": "bar"}'
Form#
Parameter: request.form
Description: The form data
FYI
The request.form
object is one of a few types that is a dictionary with each value being a list. This is because HTTP allows a single key to be reused to send multiple values.
Most of the time you will want to use the .get()
method to access the first element and not a list. If you do want a list of all items, you can use .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']
Uploaded#
Parameter: request.files
Description: The files uploaded to the server
FYI
The request.files
object is one of a few types that is a dictionary with each value being a list. This is because HTTP allows a single key to be reused to send multiple values.
Most of the time you will want to use the .get()
method to access the first element and not a list. If you do want a list of all items, you can use .getlist()
.
$ curl -F 'my_file=@/path/to/TEST' http://localhost:8000
>>> 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 context#
The request.ctx
object is your playground to store whatever information you need to about the request. This lives only for the duration of the request and is unique to the request.
This can be constrasted with the app.ctx
object which is shared across all requests. Be careful not to confuse them!
The request.ctx
object by default is a SimpleNamespace
object allowing you to set arbitrary attributes on it. Sanic will not use this object for anything, so you are free to use it however you want without worrying about name clashes.
Typical use case#
This is often used to store items like authenticated user details. We will get more into middleware later, but here is a simple example.
@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}")
As you can see, the request.ctx
object is a great place to store information that you need to access in multiple handlers making your code more DRY and easier to maintain. But, as we will learn in the middleware section, you can also use it to store information from one middleware that will be used in another.
Connection context#
Often times your API will need to serve multiple concurrent (or consecutive) requests to the same client. This happens, for example, very often with progressive web apps that need to query multiple endpoints to get data.
The HTTP protocol calls for an easing of overhead time caused by the connection with the use of keep alive headers.
When multiple requests share a single connection, Sanic provides a context object to allow those requests to share state.
@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
Warning
While this looks like a convenient place to store information between requests by a single HTTP connection, do not assume that all requests on a single connection came from a single end user. This is because HTTP proxies and load balancers can multiplex multiple connections into a single connection to your server.
DO NOT use this to store information about a single user. Use the request.ctx
object for that.
Custom Request Objects#
As dicussed in application customization, you can create a subclass of sanic.request.Request
to add additional functionality to the request object. This is useful for adding additional attributes or methods that are specific to your application.
For example, imagine your application sends a custom header that contains a user ID. You can create a custom request object that will parse that header and store the user ID for you.
from sanic import Sanic, Request
class CustomRequest(Request):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user_id = self.headers.get("X-User-ID")
app = Sanic("Example", request_class=CustomRequest)
Now, in your handlers, you can access the user_id
attribute.
@app.route("/")
async def handler(request: CustomRequest):
return text(f"User ID: {request.user_id}")
Custom Request Context#
By default, the request context (request.ctx
) is a Simplenamespace
object allowing you to set arbitrary attributes on it. While this is super helpful to reuse logic across your application, it can be difficult in the development experience since the IDE will not know what attributes are available.
To help with this, you can create a custom request context object that will be used instead of the default SimpleNamespace
. This allows you to add type hints to the context object and have them be available in your IDE.
Start by subclassing the sanic.request.Request
class to create a custom request type. Then, you will need to add a make_context()
method that returns an instance of your custom context object. NOTE: the make_context
method should be a static method.
from sanic import Sanic, Request
from types import SimpleNamespace
class CustomRequest(Request):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.ctx.user_id = self.headers.get("X-User-ID")
@staticmethod
def make_context() -> CustomContext:
return CustomContext()
@dataclass
class CustomContext:
user_id: str = None
Note
This is a Sanic poweruser feature that makes it super convenient in large codebases to have typed request context objects. It is of course not required, but can be very helpful.
Added in v23.6
Parameters#
Values that are extracted from the path parameters are injected into the handler as argumets, or more specifically as keyword arguments. There is much more detail about this in the Routing section.
@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))
Arguments#
There are two attributes on the request
instance to get query parameters:
request.args
request.query_args
These allow you to access the query parameters from the request path (the part after the ?
in the URL).
Typical use case#
In most use cases, you will want to use the request.args
object to access the query parameters. This will be the parsed query string as a dictionary.
This is by far the most common pattern.
Consider the example where we have a /search
endpoint with a q
parameter that we want to use to search for something.
@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}")
Parsing the query string#
Sometimes, however, you may want to access the query string as a raw string or as a list of tuples. For this, you can use the request.query_string
and request.query_args
attributes.
It also should be noted that HTTP allows multiple values for a single key. Although request.args
may seem like a regular dictionary, it is actually a special type that allows for multiple values for a single key. You can access this by using the request.args.getlist()
method.
request.query_string
- The raw query stringrequest.query_args
- The parsed query string as a list of tuplesrequest.args
- The parsed query string as a special dictionaryrequest.args.get()
- Get the first value for a key (behaves like a regular dictionary)request.args.getlist()
- Get all values for a key
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
FYI
The request.args
object is one of a few types that is a dictionary with each value being a list. This is because HTTP allows a single key to be reused to send multiple values.
Most of the time you will want to use the .get()
method to access the first element and not a list. If you do want a list of all items, you can use .getlist()
.
Current request getter#
Sometimes you may find that you need access to the current request in your application in a location where it is not accessible. A typical example might be in a logging
format. You can use Request.get_current()
to fetch the current request (if any).
Remember, the request object is confined to the single asyncio.Task
that is running the handler. If you are not in that task, there is no request object.
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)
In this example, we are adding the request.id
to every access log message.
Added in v22.6