跳到主要内容

response

Lihil responses are described by the function return type. You can adjust status codes, content types, and serialization using return marks and typing annotations.

Return Marks​

Use return marks to control content type and shape of the response. For example, to change the status code:

from lihil import Annotated, status

async def create_user(user: UserData, engine: Engine) -> Annotated[UserDB, status.Created]:
...

Now create_user returns status code 201 instead of the default 200.

Common return marks:

Return MarkPurposeType Argument(s)NotesExample
Json[T]Response with application/json content typeT: response body typeDefault return type if not specifiedJson[list[int]]
Stream[T]Generic chunked streaming responseT: item typeFor non-SSE streaming; use EventStream for SSE (see SSE)Stream[str]
TextPlain text response with text/plain content typeNoneUse for simple text responsesText
HTMLHTML response with text/html content typeNoneUse for HTML contentHTML
EmptyEmpty response (no body)NoneIndicates no content to returnEmpty

Example:

from lihil import Json

async def demo() -> Json[list[int]]: ...

Return marks have no runtime/typing effect outside of lihil; your type checker treats Json[T] as T.

Note: For Server-Sent Events, prefer EventStream and yield SSE events. See SSE (Server-Sent Events).

Status Codes​

  • Annotated[T, status.OK] produces a response with status code 200. T can be any JSON-serializable type or another return mark.
  • By default, the return value is JSON-serialized. For example, Annotated[UserDB, status.Created] is equivalent to Annotated[Json[UserDB], status.Created].
  • To return HTML content, use HTML:
async def hello() -> HTML:
return "<p>hello, world!</p>"

Union Returns​

You can return a union of multiple types; they appear as anyOf schemas in the OpenAPI specification.

async def create_user() -> User | TemporaryUser: ...

Custom Encoder/Decoder​

You can provide custom encoders/decoders for request parameters and function returns.

def encoder_user_id(user_id: UUID) -> bytes:
return str(user_id)

def decoder_user_id(user_id: str) -> UUID:
return UUID(user_id)

user_route = Route("/users/{user_id}")

@user_route(encoder=encode_user_id)
async def get_user(
user_id: Annotated[UUID, Param(decoder=decode_user_id)]
) -> str:
return user_id
def decoder[T](param: str | bytes) -> T: ...
  • decoder expects a single parameter with type either str (for non-body params) or bytes (for body params) and returns the required type.
def encoder[T](param: T) -> bytes: ...
  • encoder expects a single parameter of the type the endpoint returns and must return bytes.

Low-level Response Types​

In advanced cases you can use low-level response classes directly.

  • Import from lihil.vendors: Response, StreamingResponse, PlainTextResponse.
  • This is often unnecessary because return marks (Json, Text, HTML, Stream, Empty) and typed returns cover most needs.

Example:

from lihil.vendors import PlainTextResponse

async def raw_text() -> PlainTextResponse:
return PlainTextResponse("ok")

Implementation detail: lihil currently uses starlette.Response under the hood. Treat this as an internal detail. lihil may replace starlette.Response with its own implementation in the future; such a change is not considered breaking as long as the public interface and behavior remain consistent.

Behavior note: if your endpoint returns an instance of Response (or any subclass), lihil sends it as-is without further modification. Return marks (e.g., HTML, Json, Text) and status annotations on the function signature do not apply in this case; the explicit Response instance fully determines status code, headers, and body.

SSE (Server-Sent Events)​

SSE is a unidirectional streaming protocol over HTTP where the server pushes text/event-stream messages to the browser. Each message is a sequence of lines ending with a blank line. Common fields per message:

  • event: Event name string used for dispatch; listeners register via addEventListener(name, ...). If omitted, onmessage receives the event.
  • data: The message payload. Multiple consecutive data: lines are concatenated with newlines; trailing newlines are removed.
  • id: Sets the EventSource last-event-id value for reconnection and sequencing.
  • retry: Reconnection delay in milliseconds. Non-integer values are ignored by the browser.

Return SSE by making your endpoint an async generator that yields SSE events and annotating the return type as EventStream.

Basic usage:

from lihil import Route, SSE, EventStream

sse_route = Route("/sse")

@sse_route.get
async def events() -> EventStream:
yield SSE(data={"message": "Hello"}, event="start", id="1")
for i in range(3):
yield SSE(data={"count": i}, event="update", id=str(i))
yield SSE(data={"message": "Goodbye"}, event="close", id="final")

Behavior:

  • Lihil automatically wraps the generator into a streaming response with media type text/event-stream.
  • Each yielded SSE value is encoded per the SSE spec and flushed as a separate message.
  • SSE has fields event, data, id, retry; only provided fields are emitted.
  • data defaults to an empty string. If left empty, no data: lines are emitted for that message.
    • If data is a str, it is sent as-is (no JSON encoding).
    • If data is not a str, it is JSON-encoded to a compact string.
  • Multiline strings are split into multiple data: lines; every message ends with a blank line.
  • The alias EventStream is Annotated[Stream[SSE], CustomEncoder(encode_sse)], so yielding SSE values from an EventStream endpoint applies encode_sse automatically.

Browser client example:

<script>
const es = new EventSource("/sse");
es.addEventListener("update", (e) => {
const payload = JSON.parse(e.data);
console.log("count:", payload.count);
});
es.addEventListener("close", () => es.close());
es.onmessage = (e) => console.log("message:", e.data);
es.onerror = (e) => console.error("SSE error", e);
</script>

Low-level encoding (optional):

from lihil import SSE
from lihil.interface.struct import encode_sse

raw: bytes = encode_sse(SSE(data={"message": "Hello"}, event="start", id="1", retry=5000))

Examples:

SSE(data="hello")            -> data: hello\n\n
SSE(data={"x": 1}) -> data: {"x":1}\n\n
SSE(event="tick", id="42") -> event: tick\n\n id: 42\n\n
SSE() -> \n\n # no data/event/id/retry lines, just message delimiter

Testing tips:

  • Under Starlette TestClient, iterate the streamed body using iter_lines() or iter_content().
  • As of v0.2.27, cross-event-loop issues with streaming were fixed; SSE works reliably in mixed async environments.