Implement dict-like API on request objects for custom data. (#1666)
* Implement dict-like API on request objects for custom data. * Updated docs about custom_context.
This commit is contained in:
parent
6fc3381229
commit
c54a8b10bb
|
@ -39,8 +39,8 @@ app = Sanic(__name__)
|
||||||
|
|
||||||
@app.middleware('request')
|
@app.middleware('request')
|
||||||
async def add_key(request):
|
async def add_key(request):
|
||||||
# Add a key to request object like dict object
|
# Arbitrary data may be stored in request context:
|
||||||
request['foo'] = 'bar'
|
request.ctx.foo = 'bar'
|
||||||
|
|
||||||
|
|
||||||
@app.middleware('response')
|
@app.middleware('response')
|
||||||
|
@ -53,16 +53,21 @@ async def prevent_xss(request, response):
|
||||||
response.headers["x-xss-protection"] = "1; mode=block"
|
response.headers["x-xss-protection"] = "1; mode=block"
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def index(request):
|
||||||
|
return sanic.response.text(request.ctx.foo)
|
||||||
|
|
||||||
|
|
||||||
app.run(host="0.0.0.0", port=8000)
|
app.run(host="0.0.0.0", port=8000)
|
||||||
```
|
```
|
||||||
|
|
||||||
The above code will apply the three middleware in order. The first middleware
|
The three middlewares are executed in order:
|
||||||
**add_key** will add a new key `foo` into `request` object. This worked because
|
|
||||||
`request` object can be manipulated like `dict` object. Then, the second middleware
|
1. The first request middleware **add_key** adds a new key `foo` into request context.
|
||||||
**custom_banner** will change the HTTP response header *Server* to
|
2. Request is routed to handler **index**, which gets the key from context and returns a text response.
|
||||||
*Fake-Server*, and the last middleware **prevent_xss** will add the HTTP
|
3. The first response middleware **custom_banner** changes the HTTP response header *Server* to
|
||||||
header for preventing Cross-Site-Scripting (XSS) attacks. These two functions
|
say *Fake-Server*
|
||||||
are invoked *after* a user function returns a response.
|
4. The second response middleware **prevent_xss** adds the HTTP header for preventing Cross-Site-Scripting (XSS) attacks.
|
||||||
|
|
||||||
## Responding early
|
## Responding early
|
||||||
|
|
||||||
|
@ -81,6 +86,16 @@ async def halt_response(request, response):
|
||||||
return text('I halted the response')
|
return text('I halted the response')
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Custom context
|
||||||
|
|
||||||
|
Arbitrary data may be stored in `request.ctx`. A typical use case
|
||||||
|
would be to store the user object acquired from database in an authentication
|
||||||
|
middleware. Keys added are accessible to all later middleware as well as
|
||||||
|
the handler over the duration of the request.
|
||||||
|
|
||||||
|
Custom context is reserved for applications and extensions. Sanic itself makes
|
||||||
|
no use of it.
|
||||||
|
|
||||||
## Listeners
|
## Listeners
|
||||||
|
|
||||||
If you want to execute startup/teardown code as your server starts or closes, you can use the following listeners:
|
If you want to execute startup/teardown code as your server starts or closes, you can use the following listeners:
|
||||||
|
|
|
@ -4,6 +4,7 @@ import warnings
|
||||||
|
|
||||||
from collections import defaultdict, namedtuple
|
from collections import defaultdict, namedtuple
|
||||||
from http.cookies import SimpleCookie
|
from http.cookies import SimpleCookie
|
||||||
|
from types import SimpleNamespace
|
||||||
from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse
|
from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse
|
||||||
|
|
||||||
from httptools import parse_url # type: ignore
|
from httptools import parse_url # type: ignore
|
||||||
|
@ -62,7 +63,7 @@ class StreamBuffer:
|
||||||
return self._queue.full()
|
return self._queue.full()
|
||||||
|
|
||||||
|
|
||||||
class Request(dict):
|
class Request:
|
||||||
"""Properties of an HTTP request such as URL, headers, etc."""
|
"""Properties of an HTTP request such as URL, headers, etc."""
|
||||||
|
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
|
@ -75,6 +76,7 @@ class Request(dict):
|
||||||
"_socket",
|
"_socket",
|
||||||
"app",
|
"app",
|
||||||
"body",
|
"body",
|
||||||
|
"ctx",
|
||||||
"endpoint",
|
"endpoint",
|
||||||
"headers",
|
"headers",
|
||||||
"method",
|
"method",
|
||||||
|
@ -104,6 +106,7 @@ class Request(dict):
|
||||||
|
|
||||||
# Init but do not inhale
|
# Init but do not inhale
|
||||||
self.body_init()
|
self.body_init()
|
||||||
|
self.ctx = SimpleNamespace()
|
||||||
self.parsed_forwarded = None
|
self.parsed_forwarded = None
|
||||||
self.parsed_json = None
|
self.parsed_json = None
|
||||||
self.parsed_form = None
|
self.parsed_form = None
|
||||||
|
@ -120,10 +123,30 @@ class Request(dict):
|
||||||
self.__class__.__name__, self.method, self.path
|
self.__class__.__name__, self.method, self.path
|
||||||
)
|
)
|
||||||
|
|
||||||
def __bool__(self):
|
def get(self, key, default=None):
|
||||||
if self.transport:
|
""".. deprecated:: 19.9
|
||||||
return True
|
Custom context is now stored in `request.custom_context.yourkey`"""
|
||||||
return False
|
return self.ctx.__dict__.get(key, default)
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
""".. deprecated:: 19.9
|
||||||
|
Custom context is now stored in `request.custom_context.yourkey`"""
|
||||||
|
return key in self.ctx.__dict__
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
""".. deprecated:: 19.9
|
||||||
|
Custom context is now stored in `request.custom_context.yourkey`"""
|
||||||
|
return self.ctx.__dict__[key]
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
""".. deprecated:: 19.9
|
||||||
|
Custom context is now stored in `request.custom_context.yourkey`"""
|
||||||
|
del self.ctx.__dict__[key]
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
""".. deprecated:: 19.9
|
||||||
|
Custom context is now stored in `request.custom_context.yourkey`"""
|
||||||
|
setattr(self.ctx, key, value)
|
||||||
|
|
||||||
def body_init(self):
|
def body_init(self):
|
||||||
self.body = []
|
self.body = []
|
||||||
|
|
|
@ -8,22 +8,72 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from json import loads
|
from json import loads
|
||||||
|
|
||||||
|
def test_custom_context(app):
|
||||||
def test_storage(app):
|
|
||||||
@app.middleware("request")
|
@app.middleware("request")
|
||||||
def store(request):
|
def store(request):
|
||||||
|
request.ctx.user = "sanic"
|
||||||
|
request.ctx.session = None
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def handler(request):
|
||||||
|
# Accessing non-existant key should fail with AttributeError
|
||||||
|
try:
|
||||||
|
invalid = request.ctx.missing
|
||||||
|
except AttributeError as e:
|
||||||
|
invalid = str(e)
|
||||||
|
return json({
|
||||||
|
"user": request.ctx.user,
|
||||||
|
"session": request.ctx.session,
|
||||||
|
"has_user": hasattr(request.ctx, "user"),
|
||||||
|
"has_session": hasattr(request.ctx, "session"),
|
||||||
|
"has_missing": hasattr(request.ctx, "missing"),
|
||||||
|
"invalid": invalid
|
||||||
|
})
|
||||||
|
|
||||||
|
request, response = app.test_client.get("/")
|
||||||
|
assert response.json == {
|
||||||
|
"user": "sanic",
|
||||||
|
"session": None,
|
||||||
|
"has_user": True,
|
||||||
|
"has_session": True,
|
||||||
|
"has_missing": False,
|
||||||
|
"invalid": "'types.SimpleNamespace' object has no attribute 'missing'",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Remove this once the deprecated API is abolished.
|
||||||
|
def test_custom_context_old(app):
|
||||||
|
@app.middleware("request")
|
||||||
|
def store(request):
|
||||||
|
try:
|
||||||
|
request["foo"]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
request["user"] = "sanic"
|
request["user"] = "sanic"
|
||||||
request["sidekick"] = "tails"
|
sidekick = request.get("sidekick", "tails") # Item missing -> default
|
||||||
|
request["sidekick"] = sidekick
|
||||||
|
request["bar"] = request["sidekick"]
|
||||||
del request["sidekick"]
|
del request["sidekick"]
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def handler(request):
|
def handler(request):
|
||||||
return json(
|
return json(
|
||||||
{"user": request.get("user"), "sidekick": request.get("sidekick")}
|
{
|
||||||
|
"user": request.get("user"),
|
||||||
|
"sidekick": request.get("sidekick"),
|
||||||
|
"has_bar": "bar" in request,
|
||||||
|
"has_sidekick": "sidekick" in request,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
request, response = app.test_client.get("/")
|
request, response = app.test_client.get("/")
|
||||||
|
|
||||||
|
assert response.json == {
|
||||||
|
"user": "sanic",
|
||||||
|
"sidekick": None,
|
||||||
|
"has_bar": True,
|
||||||
|
"has_sidekick": False,
|
||||||
|
}
|
||||||
response_json = loads(response.text)
|
response_json = loads(response.text)
|
||||||
assert response_json["user"] == "sanic"
|
assert response_json["user"] == "sanic"
|
||||||
assert response_json.get("sidekick") is None
|
assert response_json.get("sidekick") is None
|
||||||
|
|
|
@ -1499,9 +1499,6 @@ def test_request_bool(app):
|
||||||
request, response = app.test_client.get("/")
|
request, response = app.test_client.get("/")
|
||||||
assert bool(request)
|
assert bool(request)
|
||||||
|
|
||||||
request.transport = False
|
|
||||||
assert not bool(request)
|
|
||||||
|
|
||||||
|
|
||||||
def test_request_parsing_form_failed(app, caplog):
|
def test_request_parsing_form_failed(app, caplog):
|
||||||
@app.route("/", methods=["POST"])
|
@app.route("/", methods=["POST"])
|
||||||
|
|
Loading…
Reference in New Issue
Block a user