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:

python
import stario.responses as responses

Writer

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:

  1. Status code — outcome for this response (200, 404, 307, …).

  2. Headers — metadata on w.headers before the response starts (Content-Type, Cache-Control, Location, Set-Cookie, …).

  3. Body — zero or more bytes after the header block (Content-Length or chunked Transfer-Encoding).

Typical sequence:

  • Set values on w.headers while w.started is 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 with end() (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 already bytes. content_type is bytes (e.g. b"text/plain; charset=utf-8").

stario.responses helpers perform the right sequence for common cases.

Setting headers (examples)

python
from stario import Context, Writer
import 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-Length set before write_headers — non-chunked body; bytes written are sent as-is without automatic Content-Encoding on that path.

  • No Content-Length before write_headers — chunked transfer; streaming compressor may set Content-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, or None. If the response never started, a minimal reply is synthesized (200 with body, or 204 with 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: Raw Content-Type header value (include charset when 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: If end already 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:

python
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 through stario.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 absolute http / https URL.

  • 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; 204 is 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.

python
import stario.responses as responses
from stario import Context, Writer
from 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.