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:
L. Kärkkäinen 2019-09-27 00:11:31 +03:00 committed by 7
parent 6fc3381229
commit c54a8b10bb
4 changed files with 106 additions and 21 deletions

View File

@ -39,8 +39,8 @@ app = Sanic(__name__)
@app.middleware('request')
async def add_key(request):
# Add a key to request object like dict object
request['foo'] = 'bar'
# Arbitrary data may be stored in request context:
request.ctx.foo = 'bar'
@app.middleware('response')
@ -53,16 +53,21 @@ async def prevent_xss(request, response):
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)
```
The above code will apply the three middleware in order. The first middleware
**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
**custom_banner** will change the HTTP response header *Server* to
*Fake-Server*, and the last middleware **prevent_xss** will add the HTTP
header for preventing Cross-Site-Scripting (XSS) attacks. These two functions
are invoked *after* a user function returns a response.
The three middlewares are executed in order:
1. The first request middleware **add_key** adds a new key `foo` into request context.
2. Request is routed to handler **index**, which gets the key from context and returns a text response.
3. The first response middleware **custom_banner** changes the HTTP response header *Server* to
say *Fake-Server*
4. The second response middleware **prevent_xss** adds the HTTP header for preventing Cross-Site-Scripting (XSS) attacks.
## Responding early
@ -81,6 +86,16 @@ async def halt_response(request, 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
If you want to execute startup/teardown code as your server starts or closes, you can use the following listeners:

View File

@ -4,6 +4,7 @@ import warnings
from collections import defaultdict, namedtuple
from http.cookies import SimpleCookie
from types import SimpleNamespace
from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse
from httptools import parse_url # type: ignore
@ -62,7 +63,7 @@ class StreamBuffer:
return self._queue.full()
class Request(dict):
class Request:
"""Properties of an HTTP request such as URL, headers, etc."""
__slots__ = (
@ -75,6 +76,7 @@ class Request(dict):
"_socket",
"app",
"body",
"ctx",
"endpoint",
"headers",
"method",
@ -104,6 +106,7 @@ class Request(dict):
# Init but do not inhale
self.body_init()
self.ctx = SimpleNamespace()
self.parsed_forwarded = None
self.parsed_json = None
self.parsed_form = None
@ -120,10 +123,30 @@ class Request(dict):
self.__class__.__name__, self.method, self.path
)
def __bool__(self):
if self.transport:
return True
return False
def get(self, key, default=None):
""".. deprecated:: 19.9
Custom context is now stored in `request.custom_context.yourkey`"""
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):
self.body = []

View File

@ -8,22 +8,72 @@ try:
except ImportError:
from json import loads
def test_storage(app):
def test_custom_context(app):
@app.middleware("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["sidekick"] = "tails"
sidekick = request.get("sidekick", "tails") # Item missing -> default
request["sidekick"] = sidekick
request["bar"] = request["sidekick"]
del request["sidekick"]
@app.route("/")
def handler(request):
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("/")
assert response.json == {
"user": "sanic",
"sidekick": None,
"has_bar": True,
"has_sidekick": False,
}
response_json = loads(response.text)
assert response_json["user"] == "sanic"
assert response_json.get("sidekick") is None

View File

@ -1499,9 +1499,6 @@ def test_request_bool(app):
request, response = app.test_client.get("/")
assert bool(request)
request.transport = False
assert not bool(request)
def test_request_parsing_form_failed(app, caplog):
@app.route("/", methods=["POST"])