Request & Response

Zero-allocation request parsing with fluent response building, Sonic JSON, and compatibility APIs for streaming, file serving, and hijack.

Request Object

Every handler receives a *Request containing parsed HTTP data:

go
type Request struct {
    Method       string
    Path         string
    Proto        string
    Host         string
    RemoteAddr   string
    Headers      [][2]string
    Body         []byte
    StreamID     uint32
    IsH2         bool
    Params       [8]Param
    ParamCount   int
    StreamWriter StreamWriter
}
Field Type Description
Method string HTTP method: "GET", "POST", etc.
Path string Request path: "/users/42"
Proto string Protocol: "HTTP/1.1" or "HTTP/2.0"
Host string Request host: "example.com"
RemoteAddr string Client IP address
Headers [][2]string Header key-value pairs
Body []byte Request body bytes
StreamID uint32 HTTP/2 stream ID
IsH2 bool True if HTTP/2 request
Params [8]Param Route parameters (zero-alloc, fixed-size)
ParamCount int Number of params captured
StreamWriter StreamWriter Compatibility stream writer when the request path supports streamed responses; nil in the native io_uring worker runtimes

Reading Headers

Use Header() for case-insensitive header lookup:

go
s.Router.GET("/info", func(req *core.Request, resp *core.Response) {
    contentType := req.Header("Content-Type")
    auth := req.Header("Authorization")
    userAgent := req.Header("User-Agent")

    for _, h := range req.Headers {
        key, val := h[0], h[1]
        fmt.Printf("%s: %s\n", key, val)
    }
})

Route Parameters

Access named parameters from the URL path:

go
s.Router.GET("/users/:id/posts/:postId", func(req *core.Request, resp *core.Response) {
    userId := req.ParamValue("id")
    postId := req.ParamValue("postId")

    resp.String("User " + userId + ", Post " + postId)
})

req.SetParam("key", "value")

Request Body

The body is available as raw bytes on req.Body:

go
s.Router.POST("/upload", func(req *core.Request, resp *core.Response) {
    body := req.Body
    if len(body) == 0 {
        resp.Status(400).String("Empty body")
        return
    }

    resp.Status(201).String("Received")
})

Body size is enforced by MaxBodySize in the server config. Requests exceeding this limit are rejected before reaching your handler.

Response Object

The *Response provides a fluent API for building HTTP responses. Methods are chainable:

go
resp.Status(201).
    SetHeader("X-Custom", "value").
    JSON([]byte(`{"created":true}`))

Status Codes

Set HTTP status code. Defaults to 200 if not set:

go
resp.Status(200)
resp.Status(201)
resp.Status(204)
resp.Status(301)
resp.Status(400)
resp.Status(401)
resp.Status(404)
resp.Status(500)

Response Body

Multiple methods for setting the response body:

Method Content-Type Description
String(s) text/plain Plain text response
HTML(s) text/html HTML response
Bytes(b) inherited Raw byte response
JSON(b) application/json Pre-serialized JSON bytes
JSONString(s) application/json JSON from string
JSONMarshal(v) application/json Marshal any value to JSON
go
resp.String("Hello!")

resp.HTML("<h1>Welcome</h1>")

resp.JSON([]byte(`{"status":"ok"}`))

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}
resp.JSONMarshal(User{Name: "Alice", Age: 30})

JSON Responses

For high-performance JSON, use JSON() with pre-serialized bytes to avoid reflection. For convenience, JSONMarshal() handles encoding:

go
resp.JSON([]byte(`{"users":[],"total":0}`))

err := resp.JSONMarshal(map[string]any{
    "users": users,
    "total": len(users),
})

Pooled JSON

For high-performance JSON serialization, ALOS provides a pooled Sonic JSON encoder that handles acquisition, marshaling, and recycling automatically:

go
j := core.AcquireJSON()
resp.Status(200).JSON(j.Marshal(map[string]any{
    "name":   "Alice",
    "age":    30,
    "active": true,
    "tags":   []string{"admin", "user"},
}))
j.Release()

You can also marshal structs directly:

go
type User struct {
    Name   string   `json:"name"`
    Age    int      `json:"age"`
    Active bool     `json:"active"`
    Tags   []string `json:"tags"`
}

j := core.AcquireJSON()
resp.Status(200).JSON(j.Marshal(User{
    Name:   "Alice",
    Age:    30,
    Active: true,
    Tags:   []string{"admin", "user"},
}))
j.Release()

Pooled JSON Methods

Method Description
AcquireJSON() Get a pooled JSON encoder
Marshal(v any) Serialize any value to JSON bytes using Sonic
Release() Return the encoder to the pool for reuse

Response Headers

Set custom headers on the response. Headers are chainable:

go
resp.SetHeader("X-Custom", "value").
    SetHeader("Cache-Control", "no-cache").
    SetHeader("X-Request-ID", requestId).
    Status(200).
    JSON(data)

Body Read/Write

Low-level body access for advanced use cases:

Method Description
SetBody(b []byte) Replace the response body with raw bytes
GetBody() []byte Get the current response body bytes
BodyLen() int Get the current body length
go
s.Router.GET("/raw", func(req *core.Request, resp *core.Response) {
    resp.SetBody([]byte("raw bytes"))
    resp.ContentType = "application/octet-stream"
})

s.Router.GET("/transform", func(req *core.Request, resp *core.Response) {
    resp.JSON([]byte(`{"data":"original"}`))

    body := resp.GetBody()
    modified := bytes.Replace(body, []byte("original"), []byte("modified"), 1)
    resp.SetBody(modified)
})

Streaming

Use SetStreamer and Streamer() for streaming responses. When a streamer is set, the normal body is bypassed and the StreamWriter handles output directly:

The native Linux io_uring worker backends currently reject streamed responses, so req.StreamWriter is nil there and SendFile is not available on that path yet. The examples below describe the compatibility behavior.

Method Description
SetStreamer(sw StreamWriter) Enable streaming mode with the given writer
Streamer() StreamWriter Get the current streamer
IsStreamed() bool Check if response is in streaming mode
SendFile(path string, opts ...SendFileOption) Serve a file with MIME detection and range support
go
s.Router.GET("/stream", func(req *core.Request, resp *core.Response) {
    sw := req.StreamWriter
    sw.WriteHeader(200, nil, "text/event-stream")

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

Serve files with options:

go
s.Router.GET("/download", func(req *core.Request, resp *core.Response) {
    resp.SendFile("./files/report.pdf",
        core.WithAttachment("report.pdf"),
        core.WithRateLimit(100),
    )
})

See the Streaming & Files page for the full StreamWriter API and SendFile options.

Connection Hijack

Take over the raw TCP connection for custom protocols. Returns the underlying net.Conn:

HijackConn is a compatibility API. The native Linux io_uring worker backends do not allow handlers to hijack connections today, which also means hijack-based features such as WebSocket upgrade are not available on that path.

go
s.Router.GET("/hijack", func(req *core.Request, resp *core.Response) {
    conn := req.HijackConn()
    if conn == nil {
        resp.Status(500).String("hijack failed")
        return
    }
    defer conn.Close()

    conn.Write([]byte("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"))
})

When using ALOS's custom TLS 1.3, HijackConn returns a wrapper that transparently handles AEAD encryption/decryption. You read and write plaintext; the wrapper handles record-layer framing.

ESC