Responses
This page covers Writer (status, headers, body, streaming, compression), stario.responses helpers, and cookies (set_cookie, delete_cookie). Long-lived handlers coordinate disconnect and shutdown through c.alive() on Context, not on Writer.
Import helpers from their module:
import stario.responses as responsesWriter
The framework builds a Writer for each request and passes it as the second argument to your handler (async def handler(c: Context, w: Writer)). The app and HTTP stack own construction, disconnect futures, compression policy, and the transport hook — do not call Writer(...) in normal application code.
w turns decisions into bytes on the wire: status line, response headers, optional cookies, and the message body.
Response phases
HTTP separates three layers you control in order:
Status code — outcome for this response (200, 404, 307, …).
Headers — metadata on
w.headersbefore the response starts (Content-Type,Cache-Control,Location,Set-Cookie, …).Body — zero or more bytes after the header block (
Content-Lengthor chunkedTransfer-Encoding).
Typical sequence:
Set values on
w.headerswhilew.startedis false.Call
write_headers(status_code)once to send the status line and headers.Send the body with
write(data)(repeat for streaming) and finish withend()(optionally passing a last chunk).respond(body, content_type, status=200)combines negotiation,Content-Type,Content-Length, headers, and body when the full body is alreadybytes.content_typeis bytes (e.g.b"text/plain; charset=utf-8").
stario.responses helpers perform the right sequence for common cases.
Setting headers (examples)
from stario import Context, Writerimport stario.responses as responses async def with_cache(c: Context, w: Writer) -> None: w.headers.set("Cache-Control", "public, max-age=3600") responses.html(w, "<p>Hello</p>") async def stream_lines(c: Context, w: Writer) -> None: w.headers.set("Content-Type", "text/plain; charset=utf-8") w.write_headers(200) for line in ("one", "two", "three"): w.write((line + "\n").encode("utf-8")) w.end()Cookie helpers live in stario.cookies; they mutate w.headers via Set-Cookie. See Cookies.
Once headers are on the wire
After write_headers (or any path that flushes headers), you cannot change status or add headers for that exchange. Log the error, record it on c.span, and clean up server-side; the peer only sees what was already sent.
Read-only state
On w: started, completed, status_code, and closing (transport closing or closed).
On c: disconnected, shutting_down, and closing (handler work should stop when the client left or the app is draining). See Context.
App.__call__ finishes the message in finally with w.end(), or w.abort() if headers were sent and an error occurred.
Compression
Compression policy comes from ServerConfig.compression (Configuration). The writer negotiates with Accept-Encoding; supported tokens are weighted by q values; among ties Stario prefers brotli, then zstd, then gzip. Bodies below min_size skip compression on whole-body (respond) paths; chunked streams negotiate without a minimum body size. Incompressible Content-Type values skip compression on both paths.
Two framing paths matter:
Content-Lengthset beforewrite_headers— non-chunked body; bytes written are sent as-is without automaticContent-Encodingon that path.No
Content-Lengthbeforewrite_headers— chunked transfer; streaming compressor may setContent-Encoding/Vary.
respond() may compress the full body in one pass, then set Content-Length to the compressed size. If Content-Encoding is already set on w.headers, automatic negotiation is skipped.
class Writer(transport, get_date_header, on_completed, compression=<stario.http.compression.CompressionConfig object at 0x750585926b60>, accept_encoding=None)
Low-level HTTP response serializer for one request/response on a connection.
Set headers on headers, then call respond for a whole body or write_headers followed by write / end for streaming. The response helpers and Datastar build on these methods.
Handlers normally receive a writer from the framework; constructing one yourself is for advanced or test code.
Writer.abort()
Close the connection without framing a failed started response as complete.
Writer.end(data=None)
Finish the response and run the completion callback.
data: Final body bytes, orNone. If the response never started, a minimal reply is synthesized (200with body, or204with empty body).
The protocol relies on this being called exactly once per handler path so keep-alive and pipelining stay correct.
Writer.respond(body, content_type, status=200)
Send a full response in one shot (compression, Content-Length, body).
body: Final entity body bytes.content_type: RawContent-Typeheader value (includecharsetwhen needed).status: HTTP status code.
Skips negotiation if Content-Encoding is already set on headers. Uses the whole-body compression path, not per-chunk.
Writer.write(data)
Write one body chunk, sending default 200 headers first if needed.
data: Chunk of the entity body.
self for chaining.
StarioRuntime: Ifendalready completed the response.
Status defaults to 200 when streaming starts via the first write(). In chunked mode, chunk framing and optional compression are automatic.
Writer.write_headers(status_code)
Send the status line and all current headers (must be called at most once).
status_code: HTTP status for this response.
self for chaining.
StarioRuntime: If headers were already sent.
If Content-Length is set, the body must be sent as raw bytes. Otherwise the writer uses chunked encoding and may pick a streaming compressor.
class CompressionConfig(*, min_size=512, zstd_level=3, zstd_window_log=None, brotli_level=4, brotli_window_log=None, gzip_level=6, gzip_window_bits=None)
Policy for picking br / zstd / gzip from Accept-Encoding and for streaming vs whole-body responses.
Configure only via the constructor; fields are not meant for post-init mutation.
CompressionConfig.enabled_encodings()
Supported encodings in framework preference order.
CompressionConfig.make_compressor(encoding)
Build a compressor for an encoding already selected by negotiation.
CompressionConfig.may_compress(accept_encoding, *, data=None, content_type=None, streaming=False)
Return True when negotiation could pick a compressor (shared by select and Writer fast paths).
CompressionConfig.select(accept_encoding, *, data=None, content_type=None, streaming=False)
Choose one compressor from the client header and this config, or return None (identity).
Response helpers
Module stario.responses wraps Writer with helpers that set status, content type, and headers, write the body (or set up redirects and empty bodies), and finish the response in one call.
html accepts pre-encoded bytes, a str, or Stario markup (HtmlElement) rendered via stario.markup.render. text is for human-readable str only — for arbitrary bytes (files, images), use w.respond(data, b"application/octet-stream") or stream with write_headers + write. redirect requires a 3xx status; json requires JSON-serializable values; unsafe redirect URLs raise StarioError from normalized_location.
For conditional GET (304), set cache validators on w.headers before calling empty:
w.headers.set("ETag", etag)responses.empty(w, 304)redirect uses normalized_location for URL safety before setting the Location header. empty sends a zero-length body.
normalized_location(url)
Validate a redirect target and return a percent-encoded URL string.
html(w, content, status=200)
Send a complete HTML response via Writer.respond.
w: Active response writer for this request.content:bytes,str, or Stario markup nodes (nodes run throughstario.markup.render).status: HTTP status code.
Sets Content-Type: text/html; charset=utf-8 and negotiates compression like other helpers.
json(w, value, status=200)
Serialize value to compact UTF-8 JSON and finish the response.
w: Active response writer.value: JSON-serializable structure (dict,list, scalars).status: HTTP status code.
Not for NDJSON or streaming; use Writer.write for line-delimited output.
text(w, body, status=200)
Send body as UTF-8 text/plain.
w: Active response writer.body: Message body as a Unicode string.status: HTTP status code.
redirect(w, url, status=307)
Send a redirect with an empty body and a Location header.
w: Active response writer.url: App-root-relative path or absolutehttp/httpsURL.status: Redirect status (302,303,307,308, etc.).
Raises StarioError if status is not 3xx, or if url is unsafe. Uses Content-Length: 0; call before other body writes.
empty(w, status=204)
Finish with no message body (default 204 No Content).
w: Active response writer.status: Status code;204is typical for success-without-body.
Cookies
Read inbound cookies from c.req.cookies on the request. Write outbound cookies with stario.cookies.set_cookie(w, …) and stario.cookies.delete_cookie(w, …).
delete_cookie must use the same path, domain, secure, httponly, and samesite values as the original set_cookie so browsers match and clear the cookie.
import stario.responses as responsesfrom stario import Context, Writerfrom stario import cookies async def login(c: Context, w: Writer) -> None: token = "..." cookies.set_cookie(w, "sid", token, httponly=True, secure=True, path="/app") responses.empty(w)There is no get_cookie on stario.cookies — read c.req.cookies.get("sid") instead.