In sillo, sending responses is a core part of building web applications. The `Response` object provides a powerful and flexible way to construct and send HTTP responses to the client. This guide covers the various methods available for sending responses, from simple JSON to complex file downloads.
Sending Responses
Section titled “Sending Responses”Sending a response is a fundamental aspect of every HTTP request. sillo offers a well-rounded and robust framework designed to handle this process efficiently, ensuring clarity, flexibility, and performance in every interaction.
Basic Example
Section titled “Basic Example”@app.get("/users")async def getUsers(request, response): return ["John Doe","Jane Smith"]By default sillo turns JSON-serializable python data-types returned from route handlers as response which are sent to the client as json response
Returning Various Data Types
Section titled “Returning Various Data Types”You can return various data types from your route handlers:
@app.get("/users")async def getUsers(request, response): return ["John Doe","Jane Smith"]@app.get("/users")async def getUsers(request, response): return "Hello World"@app.get("/users")async def getUsers(request, response): return {"name": "John Doe", "age": 30}@app.get("/users")async def getUsers(request, response): return 200from sillo.http.status import HTTP_200_OK
@app.get("/users")async def getUsers(request, response): return HTTP_200_OKThe Response Object
Section titled “The Response Object”for complex responses you can use the Response object
the response object is passed as the second argument to your route handlers
@app.get("/users")async def getUsers(request, response): return response.json(["John Doe","Jane Smith"])Other Response Types
@app.get("/users")async def getUsers(request, response): return response.json(["John Doe","Jane Smith"])@app.get("/users")async def getUsers(request, response): return response.html("Hello World")@app.get("/users")async def getUsers(request, response): return response.text("Hello World")@app.get("/users")async def getUsers(request, response): return response.file("path/to/file.txt")@app.get("/users")async def getUsers(request, response): return response.redirect("/users")@app.get("/users")async def getUsers(request, response): async def stream(): for i in range(10): yield f"{i}\n" return response.stream(stream())Sending Status Code
Section titled “Sending Status Code”response object has a status method that allows you to set the status code of the response.
@app.get("/users")async def getUsers(request, response): return response.status(200).json(["John Doe","Jane Smith"])Chainable Responses
Section titled “Chainable Responses”You can chain multiple methods together to configure the response before sending it. This makes your code more concise and expressive.
from sillo import silloApp
app = silloApp()
@app.get("/")async def home(request, response): response.status(200).set_cookie("session_id", "123").json({"message": "Hello, World!"})Aborting & Not Found
Section titled “Aborting & Not Found”Sometimes you don’t want to build an error response — you want to stop processing immediately with an HTTP error. The response object provides two short-circuit helpers that raise instead of returning a value:
response.abort(status_code, detail=...)— raise a genericHTTPException.response.not_found(detail=...)— raise aNotFoundException(HTTP 404).
Because they raise, the framework’s exception middleware catches them and
renders a consistent error envelope (JSON by default; HTML for 404 when
configured). You never call return after them.
response.abort()
Section titled “response.abort()”from sillo import silloApp
app = silloApp()
@app.get("/admin")async def admin(request, response): if not request.user.is_admin: response.abort(403, detail="Admins only") return response.json({"ok": True})Calling response.abort(403, detail="Admins only") raises an
HTTPException(status_code=403, detail="Admins only"). The client receives a
403 with a JSON body of "Admins only" (the detail is serialized directly).
You can also attach headers to the raised exception:
@app.post("/login")async def login(request, response): if not await authenticate(request): response.abort(401, detail="Invalid credentials", headers={"WWW-Authenticate": "Bearer"}) return response.json({"ok": True})response.not_found()
Section titled “response.not_found()”not_found() is a 404 shorthand. It raises NotFoundException, which the
framework routes through the registered 404 handler (supporting JSON, HTML, or
plain text based on configuration).
@app.get("/items/{item_id:int}")async def get_item(request, response, item_id: int): item = await db.get(item_id) if item is None: response.not_found(detail=f"Item {item_id} not found") return response.json(item)A request to /items/99 when no such item exists returns 404 with a JSON
body such as {"status": 404, "error": "Not Found", "message": "Item 99 not found"}.
When to use abort vs building a response
Section titled “When to use abort vs building a response”Use abort / not_found when you want the framework’s error handling to
apply (consistent envelopes, the configured 404 page, and centralized
logging). Use response.status(404).json(...) when you need a fully custom
body that bypasses the exception handlers.
Sending Different Types of Responses using the object directly
Section titled “Sending Different Types of Responses using the object directly”sillo provides several methods for sending different types of responses.
JSON Responses
Section titled “JSON Responses”To send a JSON response, use the .json() method. It automatically sets the Content-Type header to application/json.
@app.get("/users")async def get_users(request, response): users = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}] response.json(users)HTML Responses
Section titled “HTML Responses”To send an HTML response, use the .html() method. This will set the Content-Type header to text/html.
@app.get("/welcome")async def welcome(request, response): html_content = "<h1>Welcome to our website!</h1>" response.html(html_content)Plain Text Responses
Section titled “Plain Text Responses”For plain text responses, use the .text() method. The Content-Type will be set to text/plain.
@app.get("/status")async def status(request, response): response.text("Service is running.")Redirects
Section titled “Redirects”To redirect the client to a different URL, use the .redirect() method.
@app.get("/old-path")async def old_path(request, response): response.redirect("/new-path", status_code=301) # Permanent redirectYou can also redirect by route name instead of URL:
@app.get("/user/{user_id}", name="user_profile")async def get_user(request, response): response.json({"user_id": request.path_params.get("user_id")})
@app.get("/users")async def list_users(request, response): # Redirect by route name - generates absolute URL response.redirect(name="user_profile", user_id=42)Customizing the Response
Section titled “Customizing the Response”You can customize the response by setting the status code, headers, and cookies.
Setting the Status Code
Section titled “Setting the Status Code”Use the .status() method to set the HTTP status code.
@app.post("/create-user")async def create_user(request, response): # some logic to create a user response.status(201).json({"message": "User created successfully"})Setting Headers
Section titled “Setting Headers”Use the .set_header() method to add or modify HTTP headers.
@app.get("/data")async def get_data(request, response): response.set_header("Cache-Control", "no-cache").json({"data": "some data"})Setting Cookies
Section titled “Setting Cookies”Use the .set_cookie() method to set a cookie on the client’s browser.
@app.post("/login")async def login(request, response): response.set_cookie( key="user_token", value="secret-token", httponly=True, max_age=3600 # 1 hour ).json({"message": "Logged in"})File Responses
Section titled “File Responses”sillo allows you to send files as responses using the .file() method. This is useful for serving images, documents, or other static assets.
@app.get("/download-report")async def download_report(request, response): file_path = "path/to/your/report.pdf" response.file(file_path, content_disposition_type="attachment")By setting content_disposition_type="attachment", you prompt the browser to download the file instead of displaying it.
Returning Data Directly
Section titled “Returning Data Directly”For simple cases, you can return a dict, list, or str directly from your handler. sillo will automatically convert it into a JSON response.
@app.get("/simple")async def simple_response(request, response): return {"message": "This is a simple response."}Advanced Usage: Response Classes
Section titled “Advanced Usage: Response Classes”For more advanced use cases, sillo allows you to work directly with Response classes. This gives you the ultimate flexibility to control the response sent to the client. You can either use the built-in response classes or create your own.
Using Built-in Response Classes
Section titled “Using Built-in Response Classes”Instead of using the response object’s methods, you can return an instance of a response class directly from your handler. sillo provides several built-in response classes in the sillo.http.response module.
JSONResponseHTMLResponsePlainTextResponseRedirectResponseFileResponseStreamingResponse
Example:
from sillo import silloAppfrom sillo.http.response import JSONResponse, HTMLResponse
app = silloApp()
@app.get("/users-json")async def get_users_json(request, response): users = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}] return JSONResponse(users, status_code=200)
@app.get("/welcome-html")async def welcome_html(request, response): html_content = "<h1>Welcome from a Response class!</h1>" return HTMLResponse(html_content)Creating Custom Response Classes
Section titled “Creating Custom Response Classes”You can create your own custom response classes by inheriting from sillo.http.response.BaseResponse. This is useful when you need to send responses in a format that is not supported out of the box, such as XML.
Example: Creating an XMLResponse class
from sillo import silloAppfrom sillo.http.response import BaseResponsefrom dicttoxml import dicttoxml
class XMLResponse(BaseResponse): def __init__(self, content, *args, **kwargs): xml_content = dicttoxml(content) # BaseResponse stores the already-encoded body and a content_type super().__init__(body=xml_content, content_type="application/xml", *args, **kwargs)
app = silloApp()
@app.get("/data.xml")async def get_xml_data(request, response): data = {"user": {"name": "John Doe", "id": "123"}} return XMLResponse(data)In this example:
- We create a new
XMLResponseclass that inherits fromsillo.http.response.BaseResponse. - In the
__init__method we convert the incomingdictto XML bytes withdicttoxml, then hand the encoded body andcontent_typeto the parent class. - Finally, we return an instance of our
XMLResponsefrom the route handler.
By creating custom response classes, you can encapsulate response logic and reuse it across your application, leading to cleaner and more maintainable code.
Choosing a status code
Section titled “Choosing a status code”The code is the part of a response that clients branch on, and a handful of distinctions carry most of the weight.
201 versus 200. A created resource is a 201 with a Location header
pointing at it. Returning 200 loses the client’s ability to follow the
new resource without constructing the URL themselves.
202 versus 200. Work that has been accepted but not finished is a 202. Returning 200 tells the client the work is done, and they will believe you.
204 versus 200 with an empty body. A 204 says “no content, do not parse”. Some clients will attempt to parse an empty 200 body and throw.
404 versus 403. A 404 for a resource that exists but is not yours hides its existence, which is sometimes what you want; a 403 confirms it exists. Pick deliberately — the choice is an information-disclosure decision, not a cosmetic one.
409 versus 422. A conflict with current state is a 409; an unacceptable value is a 422. Returning 422 for “email already taken” tells a client to fix a correctly-formatted email.
500 versus 503. A 500 is your bug and retrying will not help. A 503
is temporary and retryable, and pairs with Retry-After. Clients back
off on 503 and give up on 500, which is exactly the behaviour you want
from each.
Headers worth setting deliberately
Section titled “Headers worth setting deliberately”Cache-Control decides whether anything between you and the client keeps
a copy. Its absence is not “no caching” — intermediaries apply heuristics.
Say what you mean, especially no-store on anything authenticated.
Content-Disposition decides whether a browser renders or downloads.
attachment on user-supplied files prevents an uploaded HTML file
executing with your origin’s cookies.
X-Content-Type-Options: nosniff stops browsers second-guessing your
content type, which is what turns a mislabeled upload into script
execution.
Location on 201 and on redirects. Relative is fine and avoids leaking
your internal hostname.