Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 24 additions & 9 deletions docs/sanic/middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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

Expand All @@ -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:
Expand Down
33 changes: 28 additions & 5 deletions sanic/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

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
Expand Down Expand Up @@ -71,7 +72,7 @@ def is_full(self):
return self._queue.full()


class Request(dict):
class Request:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change will break any and all current usage where someone is iterating over the request object like for _key in request. Is there a reason why these changes is required?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@harshanarayana Is this actually being used? Iteration, or the entire storage feature? Request is not a dictionary, so it shouldn't inherit just to gain those features. This has led to problems as pointed out in #1309.

The iteration APIs of dict could be added, but frankly I'd rather do what @abuckenheimer suggested and make it a public property instead. If anything, I'd expect request[...] to fetch me HTTP headers (but keeping that at request.headers[...] is better, just like request.storage[...]).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't tell you if it is being used or not. I mean the iterable nature of it. This was how it was working. So before we break this, it might be a good idea to give out a deprecation in the next release and then remove it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea iteration is weird but it actually worked properly in testing (only iterating over custom key/values) so I think we should just add a deprecated:

def __iter__(self):
    return iter(self.custom_context.__dict__)

to keep backwards compatibility

"""Properties of an HTTP request such as URL, headers, etc."""

__slots__ = (
Expand All @@ -84,6 +85,7 @@ class Request(dict):
"_socket",
"app",
"body",
"ctx",
"endpoint",
"headers",
"method",
Expand Down Expand Up @@ -113,6 +115,7 @@ def __init__(self, url_bytes, headers, version, method, transport, app):

# Init but do not inhale
self.body_init()
self.ctx = SimpleNamespace()
self.parsed_forwarded = None
self.parsed_json = None
self.parsed_form = None
Expand All @@ -129,10 +132,30 @@ def __repr__(self):
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`"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed there is a commit doing Rename request.custom_context to ctx, so the custom_context in here should be rename to ctx?

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 = []
Expand Down
56 changes: 53 additions & 3 deletions tests/test_request_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,72 @@
except ImportError:
from json import loads

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'",
}


def test_storage(app):
# Remove this once the deprecated API is abolished.
def test_custom_context_old(app):
@app.middleware("request")
def store(request):
try:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should copy this test to make another that uses request.storage directly

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
Expand Down
3 changes: 0 additions & 3 deletions tests/test_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1499,9 +1499,6 @@ def handler(request):
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"])
Expand Down