Relevant source files
The following files were used as context for generating this wiki page:
- src/helper/streaming/sse.ts
- src/helper/streaming/text.ts
- src/jsx/streaming.ts
- src/jsx/components.ts
- src/helper/streaming/stream.ts
- src/adapter/aws-lambda/handler.ts
- src/utils/stream.ts
- src/request.ts
- src/helper/ssg/ssg.ts
- src/helper/proxy/index.ts
- src/helper/streaming/index.ts
- src/client/client.ts
- src/middleware/etag/digest.ts
- src/client/fetch-result-please.ts
- src/context.ts
- src/middleware/body-limit/index.ts
- runtime-tests/lambda/stream-mock.ts
- src/adapter/deno/websocket.ts
- src/adapter/lambda-edge/handler.ts
- src/middleware/compress/index.ts
- runtime-tests/lambda/mock.ts
- src/adapter/bun/websocket.ts
- src/middleware/jsx-renderer/index.ts
- src/helper/streaming/utils.ts
- src/jsx/dom/server.ts
- src/helper/html/index.ts
- src/utils/concurrent.ts
- src/helper/websocket/index.ts
- src/adapter/cloudflare-workers/websocket.ts
- src/client/utils.ts
Overview
Hono provides robust streaming primitives and specialized helpers for real-time data delivery, chunked data pipelines, and server-sent events across modern web runtimes. By leveraging underlying Web Standard TransformStream and ReadableStream primitives, these APIs manage low-level connection lifecycles, backpressure, and abort signals seamlessly without blocking execution.
Sources: src/utils/stream.ts:6-46, src/helper/streaming/sse.ts:72-99, src/helper/streaming/stream.ts:7-13
Core Streaming Architecture and Backpressure
Plumbing and State Management
Hono's streaming infrastructure centers around the StreamingApi class, which bridges Web Standard TransformStream, WritableStream, and ReadableStream instances to manage chunk delivery and backpressure.
The StreamingApi constructor accepts both a writable and an internal _readable stream, immediately acquiring a default writer via writable.getWriter() and a reader via _readable.getReader(). It exposes a public responseReadable ReadableStream whose pull() controller reads chunks from the underlying reader, propagating backpressure signals naturally from the consumer to the producer.
export class StreamingApi {
private writer: WritableStreamDefaultWriter<Uint8Array>
private encoder: TextEncoder
private writable: WritableStream
private abortSubscribers: (() => void | Promise<void>)[] = []
responseReadable: ReadableStream
/**
* Whether the stream has been aborted.
*/
aborted: boolean = false
/**
* Whether the stream has been closed normally.
*/
closed: boolean = false
constructor(writable: WritableStream, _readable: ReadableStream) {
this.writable = writable
this.writer = writable.getWriter()
this.encoder = new TextEncoder()
const reader = _readable.getReader()
// in case the user disconnects, let the reader know to cancel
// this in-turn results in responseReadable being closed
// and writeSSE method no longer blocks indefinitely
this.abortSubscribers.push(async () => {
await reader.cancel()
})
this.responseReadable = new ReadableStream({
async pull(controller) {
const { done, value } = await reader.read()
done ? controller.close() : controller.enqueue(value)
},
cancel: () => {
if (!this.closed) {
this.abort()
}
},
})
}Sources: src/utils/stream.ts:6-46
Note
When piping an external ReadableStream into StreamingApi via pipe(body), the internal writer lock is released using writer.releaseLock() before calling body.pipeTo(this.writable, { preventClose: true }), and a new writer is acquired afterward.
async pipe(body: ReadableStream) {
this.writer.releaseLock()
await body.pipeTo(this.writable, { preventClose: true })
this.writer = this.writable.getWriter()
}Sources: src/utils/stream.ts:78-82
Abort Signals and Runtimes
Connection aborts and cancellation requests trigger subscribers registered in abortSubscribers. When responseReadable is canceled or an external abort occurs, StreamingApi.abort() iterates through all subscribers, invoking functions such as reader.cancel() to unblock pending writes and close the pipeline.
onAbort(listener: () => void | Promise<void>) {
this.abortSubscribers.push(listener)
}
/**
* Abort the stream.
* You can call this method when stream is aborted by external event.
*/
abort() {
if (!this.aborted) {
this.aborted = true
this.abortSubscribers.forEach((subscriber) => subscriber())
}
}Sources: src/utils/stream.ts:84-97
Warning
Versions of Bun prior to v1.1.27 do not automatically call cancel() on ReadableStream objects returned by Bun.serve(). Hono works around this by listening to request signal abort events on older runtimes to manually invoke stream.abort().
// Until Bun v1.1.27, Bun didn't call cancel() on the ReadableStream for Response objects from Bun.serve()
if (isOldBunVersion()) {
c.req.raw.signal.addEventListener('abort', () => {
if (!stream.closed) {
stream.abort()
}
})
}Sources: src/helper/streaming/stream.ts:15-22
Binary and Chunked Stream Helper
Public Stream API and Response Creation
The public stream helper function initializes a chunked response workflow by constructing a TransformStream, instantiating a StreamingApi instance, and returning a response via c.newResponse(stream.responseReadable). It accepts a callback function cb receiving the active StreamingApi and an optional onError handler.
export const stream = (
c: Context,
cb: (stream: StreamingApi) => Promise<void>,
onError?: (e: Error, stream: StreamingApi) => Promise<void>
): Response => {
const { readable, writable } = new TransformStream()
const stream = new StreamingApi(writable, readable)Sources: src/helper/streaming/stream.ts:7-13, src/helper/streaming/stream.ts:44-44
Note
In runtimes such as Bun, the context object c is destroyed immediately when the initial response is returned. To prevent premature garbage collection, Hono stores the context reference in a WeakMap (contextStash) keyed by stream.responseReadable until streaming completes.
// in bun, `c` is destroyed when the request is returned, so hold it until the end of streaming
contextStash.set(stream.responseReadable, c)
;(async () => {Sources: src/helper/streaming/stream.ts:24-26
Raw Chunk Writes and Stream Control
The StreamingApi class exposes methods for transmitting raw binary and text chunks, inserting newlines, delaying execution, piping streams, and closing connections.
| Method | Parameters | Return Type | Description |
|---|---|---|---|
write | input: Uint8Array | string | Promise<StreamingApi> | Encodes string inputs via TextEncoder or writes raw Uint8Array bytes to the underlying writer. |
writeln | input: string | Promise<StreamingApi> | Appends a newline character (\n) to the input string and writes it. |
sleep | ms: number | Promise<unknown> | Pauses execution for the specified milliseconds using setTimeout. |
pipe | body: ReadableStream | Promise<void> | Releases the writer lock, pipes an external readable stream into the writable target, and re-acquires the writer. |
close | none | Promise<void> | Marks the stream as closed and closes the underlying writer. |
onAbort | listener: () => void | Promise<void> | void | Registers a callback invoked when the stream aborts. |
abort | none | void | Sets the aborted flag to true and triggers all registered abort subscribers. |
async write(input: Uint8Array | string): Promise<StreamingApi> {
try {
if (typeof input === 'string') {
input = this.encoder.encode(input)
}
await this.writer.write(input)
} catch {
// Do nothing. If you want to handle errors, create a stream by yourself.
}
return this
}
async writeln(input: string): Promise<StreamingApi> {
await this.write(input + '\n')
return this
}
sleep(ms: number): Promise<unknown> {
return new Promise((res) => setTimeout(res, ms))
}
async close() {
this.closed = true
try {
await this.writer.close()
} catch {
// Do nothing. If you want to handle errors, create a stream by yourself.
}
}
async pipe(body: ReadableStream) {
this.writer.releaseLock()
await body.pipeTo(this.writable, { preventClose: true })
this.writer = this.writable.getWriter()
}
onAbort(listener: () => void | Promise<void>) {
this.abortSubscribers.push(listener)
}
/**
* Abort the stream.
* You can call this method when stream is aborted by external event.
*/
abort() {
if (!this.aborted) {
this.aborted = true
this.abortSubscribers.forEach((subscriber) => subscriber())
}
}Sources: src/utils/stream.ts:48-97
Warning
Both write and close catch and suppress write errors internally. Developers requiring custom error propagation or handling during raw chunk transmission must construct and manage custom streams directly rather than relying on the helper defaults.
try {
if (typeof input === 'string') {
input = this.encoder.encode(input)
}
await this.writer.write(input)
} catch {
// Do nothing. If you want to handle errors, create a stream by yourself.
}Sources: src/utils/stream.ts:49-56, src/utils/stream.ts:71-76
Plaintext Streaming Helper
Configuration and Semantics
The streamText helper function sets up a response specifically optimized for plain text streaming. It configures the response headers by setting Content-Type to text/plain, X-Content-Type-Options to nosniff, and Transfer-Encoding to chunked before delegating the underlying stream creation to the base stream function.
export const streamText = (
c: Context,
cb: (stream: StreamingApi) => Promise<void>,
onError?: (e: Error, stream: StreamingApi) => Promise<void>
): Response => {
c.header('Content-Type', TEXT_PLAIN)
c.header('X-Content-Type-Options', 'nosniff')
c.header('Transfer-Encoding', 'chunked')
return stream(c, cb, onError)
}Sources: src/helper/streaming/text.ts:6-15
| Header Name | Value | Purpose |
|---|---|---|
Content-Type | text/plain | Instructs the client to interpret the incoming payload as plaintext content. |
X-Content-Type-Options | nosniff | Prevents MIME-type sniffing by enforcing strict adherence to the declared content type. |
Transfer-Encoding | chunked | Enables chunked transfer encoding for streaming responses without a predefined content length. |
c.header('Content-Type', TEXT_PLAIN)
c.header('X-Content-Type-Options', 'nosniff')
c.header('Transfer-Encoding', 'chunked')Sources: src/helper/streaming/text.ts:11-13
Note
The streamText function accepts three parameters: the Hono Context object c, a mandatory callback function cb receiving the active StreamingApi instance, and an optional onError error-handling callback.
export const streamText = (
c: Context,
cb: (stream: StreamingApi) => Promise<void>,
onError?: (e: Error, stream: StreamingApi) => Promise<void>
): Response => {Sources: src/helper/streaming/text.ts:6-10
Server-Sent Events API and Lifecycle
SSEStreamingApi and Connection Setup
The SSEStreamingApi class extends StreamingApi to provide specialized formatting for Server-Sent Events messages via writeSSE. The streamSSE helper initializes this architecture by creating a TransformStream pair, instantiating SSEStreamingApi with the writable and readable ends, and establishing essential response headers for event-stream communication.
| Header Name | Value | Purpose |
|---|---|---|
Transfer-Encoding | chunked | Enables chunked transfer encoding for continuous streaming payloads. |
Content-Type | text/event-stream | Instructs the client to parse the incoming stream as Server-Sent Events. |
Cache-Control | no-cache | Prevents intermediaries and clients from caching the event stream. |
Connection | keep-alive | Keeps the underlying TCP connection open for continuous event transmission. |
Sources: src/helper/streaming/sse.ts:13-16, src/helper/streaming/sse.ts:72-96
Note
In runtime environments such as Bun, context objects can be destroyed immediately after the initial response returns. To prevent premature garbage collection, Hono stores the context reference inside a contextStash WeakMap keyed by stream.responseReadable until the stream lifecycle completes.
const contextStash: WeakMap<ReadableStream, Context> = new WeakMap<ReadableStream, Context>()Sources: src/helper/streaming/sse.ts:70-70, src/helper/streaming/sse.ts:89-91
Warning
Prior to Bun v1.1.27, runtime responses from Bun.serve() did not automatically invoke cancel() on the underlying ReadableStream. Hono mitigates this by manually listening to request abort signals on older Bun versions and triggering stream.abort() if the stream remains unclosed.
// Until Bun v1.1.27, Bun didn't call cancel() on the ReadableStream for Response objects from Bun.serve()
if (isOldBunVersion()) {
c.req.raw.signal.addEventListener('abort', () => {
if (!stream.closed) {
stream.abort()
}
})
}Sources: src/helper/streaming/sse.ts:80-87
Execution Walkthrough and Sequence
The execution lifecycle of a Server-Sent Events stream follows a strict invocation path managed by the run helper function, executing the user callback, handling errors, and ensuring resource cleanup.
Sources: src/helper/streaming/sse.ts:47-68, src/helper/streaming/sse.ts:97-97
The lifecycle operations proceed through the following steps:
streamSSEinvokesrunpassing the activeSSEStreamingApiinstance, callback, and optional error handler. Sources: src/helper/streaming/sse.ts:97-97runawaits the user callbackcb(stream). If an error occurs andonErroris provided, it executesonError(e, stream)followed by writing an error event to the stream; otherwise, errors are logged toconsole.error. Sources: src/helper/streaming/sse.ts:52-64closeis guaranteed to execute in thefinallyblock, markingthis.closed = trueand closing the underlying writer to finalize the transmission. Sources: src/helper/streaming/sse.ts:65-67, src/utils/stream.ts:69-76
Event Framing and Serialization
Execution Walkthrough
The serialization of Server-Sent Events messages is handled by the writeSSE method on the SSEStreamingApi class. This method processes incoming message payloads, validates field properties for illegal line breaks, formats them into the standard SSE wire format, and dispatches the raw bytes down the stream via streamSSE → run → writeSSE → write.
Sources: src/helper/streaming/sse.ts:18-44, src/helper/streaming/sse.ts:47-68, src/helper/streaming/sse.ts:97-97, src/utils/stream.ts:48-58
streamSSEinitializes the transformation stream and invokesrun. Sources: src/helper/streaming/sse.ts:72-97runexecutes the user-supplied callback function. Sources: src/helper/streaming/sse.ts:47-53writeSSEreceives anSSEMessageobject, resolving any asynchronous data promises and formatting fields into text blocks. Sources: src/helper/streaming/sse.ts:18-41writeencodes the formatted string into aUint8ArrayviaTextEncoderand writes it to the underlying writable stream writer. Sources: src/helper/streaming/sse.ts:43-43, src/utils/stream.ts:7-24, src/utils/stream.ts:48-58
Message Structure and Validation
The SSEMessage interface supports four specific fields used to frame events. Data payloads can be provided as a string or a Promise<string>.
| Field | Type | Description |
|---|---|---|
data | string | Promise<string> | The payload content of the message; multi-line strings are automatically prefixed with data: on each line. |
event | string (optional) | The event type identifier for the message. |
id | string (optional) | The event stream synchronization ID. |
retry | number (optional) | The reconnection time interval in milliseconds. |
Sources: src/helper/streaming/sse.ts:6-11, src/helper/streaming/sse.ts:18-41
Caution
Control character injection is strictly guarded during message serialization. If any of the tracked meta-fields (event, id, or retry) contain carriage return (\r) or newline (\n) characters, writeSSE immediately throws an error to prevent protocol corruption.
for (const key of ['event', 'id', 'retry'] as (keyof SSEMessage)[]) {
if (message[key] && /[\r\n]/.test(message[key] as string)) {
throw new Error(`${key} must not contain "\\r" or "\\n"`)
}
}Sources: src/helper/streaming/sse.ts:27-31
Related
On This Page
OverviewCore Streaming Architecture and BackpressurePlumbing and State ManagementAbort Signals and RuntimesBinary and Chunked Stream HelperPublic Stream API and Response CreationRaw Chunk Writes and Stream ControlPlaintext Streaming HelperConfiguration and SemanticsServer-Sent Events API and LifecycleSSEStreamingApi and Connection SetupExecution Walkthrough and SequenceEvent Framing and SerializationExecution WalkthroughMessage Structure and ValidationRelated