Responses¶
Lilya, by design, furnishes specific response classes that serve a dual purpose. They offer
utility and are tasked with sending the appropriate ASGI messages through the send
channel.
Lilya automatically includes the Content-Length
and Content-Type
headers.
How does it work¶
There are a few ways of using the responses within a Lylia application.
- You can import the appropriate
response
class and use it directly. - You can build the response.
- You can delegate to Lilya.
- Build a custom encoder that will allow Lilya to automatically parse the response.
Available responses¶
All the responses from Lilya inherit from the parent object Response
and that same class can
also be used directly.
All the responses are considered ASGI applications, which means you can treat them as such in your application if necessary.
Example
from lilya.responses import PlaiText
from lilya.types import Scope, Receive, Send
async def asgi_app(scope: Scope, receive: Receive, send: Send):
assert scope['type'] == 'http'
response = PlaiText('Welcome')
await response(scope, receive, send)
Response¶
from lilya.responses import Response
Example
from lilya.apps import Lilya
from lilya.responses import Response
from lilya.routing import Path
def home():
return Response("Welcome home")
app = Lilya(routes=[Path("/", home)])
Set cookie¶
Lilya provides the set_cookie
that allows settings a cookie on a given response. All the responses
available in Lilya have access to this functionality.
from lilya.responses import Response
from lilya.types import Scope, Receive, Send
async def asgi_app(scope: Scope, receive: Receive, send: Send):
assert scope['type'] == 'http'
response = Response('Welcome', media_type='text/plain')
response.set_cookie(key=..., value=..., max_age=..., expires=...,)
await response(scope, receive, send)
Parameters¶
The available parameters of the set_cookie
are as follow:
key
- A string representing the cookie's key.value
- A string representing the cookie's value.max_age
- An integer defining the cookie's lifetime in seconds. A negative value or 0 discards the cookie immediately. (Optional)expires
- Either an integer indicating the seconds until the cookie expires or a datetime. (Optional)path
- A string specifying the subset of routes to which the cookie applies. (Optional)domain
- A string specifying the valid domain for the cookie. (Optional)secure
- A boolean indicating that the cookie is sent to the server only if the request uses SSL and the HTTPS protocol. (Optional)httponly
- A boolean indicating that the cookie is inaccessible via JavaScript through Document.cookie, the XMLHttpRequest, or Request APIs. (Optional)samesite
- A string specifying the samesite strategy for the cookie, with valid values of'lax'
,'strict'
, and'none'
. Defaults to 'lax'. (Optional)
Delete cookie¶
In the same fashion as the set cookie, this function is available on every response provided by Lilya.
from lilya.responses import Response
from lilya.types import Scope, Receive, Send
async def asgi_app(scope: Scope, receive: Receive, send: Send):
assert scope['type'] == 'http'
response = Response('Welcome', media_type='text/plain')
response.delete_cookie(key=..., path=..., domain=...)
await response(scope, receive, send)
Parameters¶
The available parameters of the set_cookie
are as follow:
key
- A string representing the cookie's key.path
- A string specifying the subset of routes to which the cookie applies. (Optional)domain
- A string specifying the valid domain for the cookie. (Optional)
HTMLResponse¶
Returning an html
response.
from lilya.responses import HTMLResponse
Example
from lilya.apps import Lilya
from lilya.responses import HTMLResponse
from lilya.routing import Path
def home():
return HTMLResponse("<html><body><p>Welcome!</p></body></html>")
app = Lilya(routes=[Path("/", home)])
Error¶
Response that can be used when throwing a 500
error. Defaults to return an html
response.
from lilya.responses import Error
Example
from lilya.apps import Lilya
from lilya.responses import Error
from lilya.routing import Path
def home():
return Error("<html><body><p>Error!</p></body></html>")
app = Lilya(routes=[Path("/", home)])
PlainText¶
Response that can be used to return text/plain
.
from lilya.responses import PlainText
Example
from lilya.apps import Lilya
from lilya.responses import PlainText
from lilya.routing import Path
def home():
return PlainText("Welcome home")
app = Lilya(routes=[Path("/", home)])
JSONResponse¶
Response that can be used to return application/json
.
from lilya.responses import JSONResponse
Example
from lilya.apps import Lilya
from lilya.responses import JSONResponse
from lilya.routing import Path
def home():
return JSONResponse({"message": "Welcome home"})
app = Lilya(routes=[Path("/", home)])
Ok¶
Response that can be used to return application/json
as well. You can see this as an
alternative to JSONResponse
.
from lilya.responses import Ok
Example
from lilya.apps import Lilya
from lilya.responses import Ok
from lilya.routing import Path
def home():
return Ok({"message": "Welcome home"})
app = Lilya(routes=[Path("/", home)])
RedirectResponse¶
Used for redirecting the responses.
from lilya.responses import RedirectResponse
Example
from lilya.apps import Lilya
from lilya.responses import RedirectResponse
from lilya.routing import Path
def home():
return RedirectResponse(url="/another-url")
app = Lilya(routes=[Path("/", home)])
StreamingResponse¶
from lilya.responses import StreamingResponse
Example
from collections.abc import Generator
from lilya.apps import Lilya
from lilya.responses import StreamingResponse
from lilya.routing import Path
def my_generator() -> Generator[str, None, None]:
count = 0
while True:
count += 1
yield str(count)
def home():
return StreamingResponse(my_generator(), media_type="text/html")
app = Lilya(routes=[Path("/", home)])
FileResponse¶
from lilya.responses import FileResponse
Streams a file asynchronously as the response, employing a distinct set of arguments for instantiation compared to other response types:
path
- The filepath to the file to stream.status_code
- The Status code to return.headers
- Custom headers to include, provided as a dictionary.media_type
- A string specifying the media type. If unspecified, the filename or path is used to deduce the media type.filename
- If specified, included in the response Content-Disposition.content_disposition_type
- Included in the response Content-Disposition. Can be set toattachment
(default) orinline
.background
- A task instance.allow_range_requests
- Should enable support for http ranges? By defaultTrue
. You certainly want this for continuing downloads.range_multipart_boundary
- Enable multipart http ranges. Either bool or explicit string value used for the boundary. By defaultFalse
(multipart is disabled).
Example
from lilya.apps import Lilya
from lilya.responses import FileResponse
from lilya.routing import Path
def home():
return FileResponse(
"files/something.csv",
filename="something",
)
app = Lilya(routes=[Path("/", home)])
By default multipart ranges are disabled as it is a bit more expensive (cpu and data usage), you can enable it by setting
range_multipart_boundary
to True
or an explicit boundary value.
from lilya.apps import Lilya
from lilya.responses import FileResponse
from lilya.routing import Path
def home():
return FileResponse(
"files/something.csv",
filename="something",
range_multipart_boundary=True
)
# or alternatively provide an explicit boundary
def home():
return FileResponse(
"files/something.csv",
filename="something",
range_multipart_boundary="1234abc"
)
app = Lilya(routes=[Path("/", home)])
By default we limit the maximum amount of requested ranges to five. For a different security approach
or different multipart parsing you can modify the FileResponse
from typing import Any
from lilya.apps import Lilya
from lilya.responses import FileResponse
from lilya.routing import Path
from lilya.types import Scope
from lilya.ranges import ContentRanges
class CustomFileResponse(FileResponse):
def get_content_ranges_and_multipart(
self, scope: Scope, /, **kwargs: Any
) -> tuple[ContentRanges | None, bool]:
kwargs.setdefault("enforce_asc", False)
# unlimit the amount of requested ranges and do security later
kwargs.setdefault("max_ranges", None if self.range_multipart_boundary else 1)
content_ranges, multipart = super().get_content_ranges_and_multipart(scope, **kwargs)
# check that ranges are not too small, resource abuse by using protocol overhead
# Note: we have already a mitigation in place by allowing only strictly ascending orders even with enforce_asc=False
# enforce_asc=False is more lenient by modifying the ranges
for range_def in content_ranges.ranges:
# ranges must be at least 50 bytes otherwise the whole response is returned
if range_def.stop - range_def.start +1 <= 50:
# returning (None, ...) causes a whole response to be returned instead of a partial response
return None, False
# allow unordered ranges
# allow single range responses on multi range requests (spec violation some clients does not support)
# by default multipart is used when a "," is in the range header
return content_ranges, len(content_ranges.ranges) > 1
def home() -> CustomFileResponse:
return CustomFileResponse(
"files/something.csv",
filename="something",
)
app = Lilya(routes=[Path("/", home)])
Note however that some clients doesn't behave well (or just fallback to non-range download) if multi-range requests are answered with a single range response and vice versa.
Importing the appropriate class¶
This is the classic most used way of using the responses. The available responses contains a list of available responses of Lilya but you are also free to design your own and apply them.
Example
from lilya.apps import Lilya
from lilya.responses import JSONResponse
from lilya.routing import Path
def home():
return JSONResponse({"message": "Welcome home"})
app = Lilya(routes=[Path("/", home)])
Build the Response¶
This is where the things get great. Lilya provides a make_response
function that automatically
will build the response for you.
from lilya.responses import make_response
Example
from lilya.apps import Lilya
from lilya.responses import make_response
from lilya.routing import Path
def home():
return make_response({{"message": "Hello"}}, status_code=201)
app = Lilya(routes=[Path("/", home)])
By default, the make_response
returns a JSONResponse but that can be also
changed if the response_class
parameter is set to something else.
So, why is this make_response
different from the other responses? Well, here its where Lilya shines.
Lilya is pure Python, which means that it does not rely or depend on external libraries like Pydantic,
msgspec, attrs or any other but allows you to build a custom encoder that
can later be used to serialise your response automatically and then passed to the make_response
.
Check the build a custom encoder and custom encoders with make_response for more details and how to leverage the power of Lilya.
Async content¶
You can pass coroutines as content to most standard responses. This will delay the evaluation of the content to the __call__
method
if resolve_async_content()
is not called earlier.
The cool part, we reuse the main eventloop.
Note, this means we get the body attribute of the response as well as the content-length
header later
after the resolve_async_content()
call (which is called in __call__
).
Delegate to Lilya¶
Delegating to Lilya means that if no response is specified, Lilya will go through the internal
encoders
and will try to jsonify
the response for you.
Let us see an example.
from lilya.apps import Lilya
from lilya.routing import Path
def home():
return {"message": "Welcome home"}
app = Lilya(routes=[Path("/", home)])
As you can see, no response
was specified but instead a python dict
was returned. What Lilya
internally does is to guess and understand the type of response parse the result into json
and returning a JSONResponse
automatically,
If the type of response is not json serialisable, then a ValueError
is raised.
Let us see some more examples.
from lilya.apps import Lilya
from lilya.routing import Path
def home_dict():
return {"message": "Welcome home"}
def home_frozen_set():
return frozenset({"message": "Welcome home"})
def home_set():
return set({"message": "Welcome home"})
def home_list():
return ["Welcome", "home"]
def home_str():
return "Welcome home"
def home_int():
return 1
def home_float():
return 2.0
app = Lilya(
routes=[
Path("/dict", home_dict),
Path("/fronzenset", home_frozen_set),
Path("/set", home_set),
Path("/list", home_list),
Path("/str", home_str),
Path("/int", home_int),
Path("/float", home_float),
]
)
And the list goes on and on. Lilya by design understands almost every single datastructure of Python
by default, including Enum
, deque
, dataclasses
, PurePath
, generators
and tuple
.
This is archived by using Encoders. Mainly they are additional encoders for types tje json encoder cannot handle.
Pass body types directly to the application server¶
By default ASGI
allows only byte strings as body type.
Some servers are more lenient and allow also memoryview
, bytearray
or maybe something custom, which prevent
in best case a copy. If you want to leverage this you can use passthrough_body_types
. It is a type tuple on the response
which contains the types which are directly forwarded to the application server.
Please add bytes
always to the types tuple, otherwise if you return bytes, it could be returned base64 encoded.
To pass them through you have three options:
- Subclassing the response and add
passthrough_body_types
either as ClassVar, assignment on initialization or as property. - Some responses allow setting
passthrough_body_types
on the fly. Pass the wanted types tuple to it. - Overwrite globally the types tuple in
lilya.responses.Response
. This is not recommended for libraries only for final applications.