Streaming & Files

Compatibility reference for StreamWriter and SendFile. The native Linux io_uring worker backends currently reject streamed or hijacked responses, so these APIs are documented here with their current runtime limits.

Overview

ALOS exposes a StreamWriter interface for streamed responses. When that API is available, it uses HTTP/1.1 chunked transfer encoding or HTTP/2 DATA frames with flow control. The native Linux io_uring worker backends currently return an error instead of allowing streamed responses.

Current status: StreamWriter, SetStreamer, and SendFile are compatibility APIs today. They are not available on the native Linux io_uring worker runtimes yet.

HTTP/1.1 Chunked

Transfer-Encoding: chunked. Each WriteChunk sends a sized chunk. End with a zero-length chunk via Close.

HTTP/2 DATA Frames

Full flow control with WINDOW_UPDATE management. Automatic frame splitting at maxFrameSize. END_STREAM on close.

StreamWriter API

When a streamed response path is available, the StreamWriter interface provides protocol-agnostic streaming:

Method Description
WriteHeader(statusCode int, headers [][2]string, contentType string) Send the response status, headers, and content type. Must be called first.
WriteChunk(data []byte) error Send a chunk of data to the client
Flush() error Flush any buffered data immediately
Close() error End the stream (sends trailing chunk / END_STREAM flag)
go
type StreamWriter interface {
    WriteHeader(statusCode int, headers [][2]string, contentType string) error
    WriteChunk(data []byte) error
    Flush() error
    Close() error
}

Access the stream writer via req.StreamWriter on compatible request paths, or via resp.Streamer() / resp.SetStreamer(). On the native Linux io_uring worker runtimes, req.StreamWriter is nil.

go
r.GET("/stream", func(req *core.Request, resp *core.Response) {
    sw := req.StreamWriter

    sw.WriteHeader(200, [][2]string{
        {"Cache-Control", "no-cache"},
    }, "text/event-stream")

    for i := 0; i < 10; i++ {
        data := fmt.Sprintf("data: event %d\n\n", i)
        sw.WriteChunk([]byte(data))
        sw.Flush()
        time.Sleep(1 * time.Second)
    }

    sw.Close()
})

HTTP/1.1 Chunked Streaming

When streaming is enabled on a compatible path, HTTP/1.1 uses chunked transfer encoding. That is useful for Server-Sent Events, live logs, and progressive rendering:

Server-Sent Events (SSE)

go
r.GET("/events", func(req *core.Request, resp *core.Response) {
    sw := req.StreamWriter

    sw.WriteHeader(200, [][2]string{
        {"Cache-Control", "no-cache"},
        {"Connection", "keep-alive"},
    }, "text/event-stream")

    for i := 0; i < 50; i++ {
        data := fmt.Sprintf("id: %d\nevent: tick\ndata: {\"count\":%d}\n\n", i, i)
        if err := sw.WriteChunk([]byte(data)); err != nil {
            break
        }
        sw.Flush()
        time.Sleep(500 * time.Millisecond)
    }

    sw.Close()
})

Live Log Streaming

go
r.GET("/logs", func(req *core.Request, resp *core.Response) {
    sw := req.StreamWriter
    sw.WriteHeader(200, nil, "text/plain; charset=utf-8")

    logCh := getLogChannel()
    for line := range logCh {
        if err := sw.WriteChunk([]byte(line + "\n")); err != nil {
            break
        }
        sw.Flush()
    }

    sw.Close()
})

Streaming JSON Array

go
r.GET("/users/export", func(req *core.Request, resp *core.Response) {
    sw := req.StreamWriter
    sw.WriteHeader(200, nil, "application/json")

    sw.WriteChunk([]byte("["))
    first := true
    for _, user := range getAllUsers() {
        if !first {
            sw.WriteChunk([]byte(","))
        }
        first = false
        data, _ := json.Marshal(user)
        sw.WriteChunk(data)
    }
    sw.WriteChunk([]byte("]"))
    sw.Close()
})

HTTP/2 Streaming

When streaming is available over HTTP/2, ALOS uses DATA frames with built-in flow control and automatically:

  • Splits data into frames respecting maxFrameSize (default 16 KB)
  • Tracks the peer’s flow control window (default 1 MB per stream)
  • Blocks writes when the window is exhausted (back-pressure with 30s timeout)
  • Resumes when WINDOW_UPDATE frames arrive
  • Respects both stream-level and connection-level flow control
go
r.GET("/h2-stream", func(req *core.Request, resp *core.Response) {
    sw := req.StreamWriter
    sw.WriteHeader(200, nil, "application/octet-stream")

    bigChunk := make([]byte, 1<<20)
    sw.WriteChunk(bigChunk)
    sw.Close()
})

On compatible non-worker paths, you don't need to detect the protocol. The same StreamWriter API works for both HTTP/1.1 and HTTP/2. ALOS creates the correct implementation based on req.IsH2.

Protocol-Aware Streaming

go
r.GET("/adaptive", func(req *core.Request, resp *core.Response) {
    sw := req.StreamWriter
    sw.WriteHeader(200, nil, "application/octet-stream")

    chunkSize := 32 * 1024
    if req.IsH2 {
        chunkSize = 256 * 1024
    }

    buf := make([]byte, chunkSize)
    for {
        n, err := dataSource.Read(buf)
        if n > 0 {
            sw.WriteChunk(buf[:n])
        }
        if err != nil {
            break
        }
    }
    sw.Close()
})

File Serving

SendFile uses the same streamed-response machinery as StreamWriter. It is documented here for compatibility paths and includes automatic MIME detection for ~40 extensions, path traversal prevention, range requests, and symlink resolution:

go
r.GET("/files/*path", func(req *core.Request, resp *core.Response) {
    resp.SendFile("./public/" + req.ParamValue("path"))
})

File Download

Use WithAttachment to trigger a browser download with a specific filename:

go
r.GET("/download", func(req *core.Request, resp *core.Response) {
    resp.SendFile("alos-tls.exe",
        core.WithAttachment("alos-tls.exe"),
        core.WithRateLimit(100),
    )
})

Rate-Limited Download

go
r.GET("/limited/:file", func(req *core.Request, resp *core.Response) {
    resp.SendFile(
        "./files/" + req.ParamValue("file"),
        core.WithAttachment("download.zip"),
        core.WithRateLimit(50),
    )
})

Burst + Throttle

Allow a fast initial burst, then throttle to a steady rate:

go
resp.SendFile(
    "./files/large-archive.tar.gz",
    core.WithAttachment("large-archive.tar.gz"),
    core.WithRateLimitBurst(100, 400),
)

This allows an initial burst at 400 Mbps, then throttles to 100 Mbps for the remainder.

SendFile Options

Option Description
WithAttachment(filename string) Set Content-Disposition: attachment; filename="..." for download. Filename is the name shown to the user.
WithContentType(mime string) Override automatic MIME type detection
WithRateLimit(mbps int64) Throttle download speed in Mbps. Range: 1–250,000. Internally converts mbps * 125000 to bytes/sec.
WithRateLimitBurst(mbps, burstMbps int64) Throttle with a custom burst size in Mbps. Allows initial burst before throttling kicks in.
go
resp.SendFile("./assets/report.pdf",
    core.WithAttachment("monthly-report.pdf"),
    core.WithContentType("application/pdf"),
    core.WithRateLimit(200),
)
go
resp.SendFile("data.bin", core.WithContentType("application/x-custom"))

Security

File serving includes built-in security measures:

  • Path traversal prevention.. segments and absolute paths are rejected, preventing directory escape
  • Symlink resolution — symlinks are resolved and validated to stay within the served directory
  • MIME detection — Content-Type set from file extension (~40 built-in types), never from content sniffing
  • Range requests — supports Range headers for resumable downloads
  • Stream-based — when supported, files are streamed via StreamWriter instead of being loaded entirely into memory

Always use a dedicated directory for served files. Never pass user input directly as an absolute path to SendFile. The path is sanitized but defense-in-depth is recommended.

Bandwidth Control

When streaming is available, stream writers (H1, H2, and plain) support per-connection and global bandwidth limiting via the server config:

go
s := core.New(core.Config{
    Addr: ":443",
    ConnBandwidth: core.BandwidthConfig{
        MaxDownloadRate: 40,
        BurstSize:       16,
    },
    GlobalBandwidth: core.BandwidthConfig{
        MaxDownloadRate: 1600,
        BurstSize:       800,
    },
})

These limits apply to all streamed responses, including SendFile downloads. Use ConnBandwidth to limit per-connection speed and GlobalBandwidth for server-wide throughput.

ESC