Skip to content

Sending Responses

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 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.

@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

You can return various data types from your route handlers:

List
@app.get("/users")
async def getUsers(request, response):
return ["John Doe","Jane Smith"]
String
@app.get("/users")
async def getUsers(request, response):
return "Hello World"
Dict
@app.get("/users")
async def getUsers(request, response):
return {"name": "John Doe", "age": 30}
Int
@app.get("/users")
async def getUsers(request, response):
return 200
Enum
from sillo.http.status import HTTP_200_OK
@app.get("/users")
async def getUsers(request, response):
return HTTP_200_OK

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

JSON
@app.get("/users")
async def getUsers(request, response):
return response.json(["John Doe","Jane Smith"])
HTML
@app.get("/users")
async def getUsers(request, response):
return response.html("Hello World")
Text
@app.get("/users")
async def getUsers(request, response):
return response.text("Hello World")
File
@app.get("/users")
async def getUsers(request, response):
return response.file("path/to/file.txt")
Redirect
@app.get("/users")
async def getUsers(request, response):
return response.redirect("/users")
Streaming
@app.get("/users")
async def getUsers(request, response):
async def stream():
for i in range(10):
yield f"{i}\n"
return response.stream(stream())

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"])

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!"})

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 generic HTTPException.
  • response.not_found(detail=...) — raise a NotFoundException (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.

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})

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"}.

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.

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)

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)

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.")

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 redirect

You 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)

You can customize the response by setting the status code, headers, and cookies.

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"})

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"})

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"})

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.

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."}

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.

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.

  • JSONResponse
  • HTMLResponse
  • PlainTextResponse
  • RedirectResponse
  • FileResponse
  • StreamingResponse

Example:

from sillo import silloApp
from 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)

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 silloApp
from sillo.http.response import BaseResponse
from 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:

  1. We create a new XMLResponse class that inherits from sillo.http.response.BaseResponse.
  2. In the __init__ method we convert the incoming dict to XML bytes with dicttoxml, then hand the encoded body and content_type to the parent class.
  3. Finally, we return an instance of our XMLResponse from 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.

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.

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.