Request & Response

> The *router.Context API - route parameters, query strings, headers, cookies, JSON and form binding, responses, files, streaming, redirects, errors, and service accessors.

Every handler has the signature func(*router.Context) error. The *router.Context wraps the request and response and carries the helpers below. Route definition, groups, and named routes are on Routing; handler organisation is on Handlers.

Import path: github.com/velocitykode/velocity/router

*router.Context wraps the request, response, and helpers. Every handler has signature func(*router.Context) error.

Reading parameters

id := c.Param("id")              // string
n, err := c.ParamInt("page")     // int
big, err := c.ParamInt64("id")   // int64

Query strings

q       := c.Query("q")                       // string
sort    := c.QueryDefault("sort", "newest")    // string with default
page    := c.QueryInt("page", 1)              // int with default
limit   := c.QueryInt64("limit", 25)          // int64 with default
amount  := c.QueryFloat64("amount", 0.0)      // float64 with default
verbose := c.QueryBool("verbose")             // accepts 1/0, true/false, t/f, etc.

Headers

ua    := c.Header("User-Agent")               // read
size  := c.HeaderInt64("Content-Length", 0)   // read as int64

c.SetHeader("X-Request-ID", requestID)        // write (replaces)
c.AddHeader("Vary", "Accept")                 // append (list-valued)

SetHeader and AddHeader reject names or values containing CR/LF to prevent header injection.

Cookies

sess, err := c.Cookie("session_id")

c.SetCookie(&http.Cookie{
    Name:     "session_id",
    Value:    id,
    Path:     "/",
    HttpOnly: true,
    Secure:   true,
})

JSON binding

c.Bind decodes the request body as JSON:

type CreateUser struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

r.Post("/users", func(c *router.Context) error {
    var req CreateUser
    if err := c.Bind(&req); err != nil {
        return c.BadRequest("invalid body")
    }
    // ...
    return c.JSON(http.StatusCreated, req)
})

The body is wrapped with a 10MB limit by default (router.DefaultMaxBodySize). Routes that must accept larger payloads should install the router.BodyLimit(N) middleware for their chain; when present it sets the cap for Bind, BindForm, BindXML, FormValue, and FormFile instead of the default.

Form data

c.FormValue reads a single URL-encoded or multipart value (capping the body at DefaultMaxBodySize unless BodyLimit is installed), and c.FormFile returns the first uploaded file for a key:

name := c.FormValue("name")

fh, err := c.FormFile("avatar")
if err != nil {
    return c.BadRequest("invalid upload")
}

To map an entire form into a struct, use c.BindForm (form tags), c.BindQuery (query tags), c.BindXML, or c.BindAuto (which picks a binder from the Content-Type, defaulting to JSON):

type Filter struct {
    Sort string `form:"sort"`
    Page int    `form:"page"`
}

var f Filter
if err := c.BindForm(&f); err != nil {
    return c.BadRequest("invalid form")
}

c.BindValid binds JSON and then runs the struct’s own ValidationRules() if it implements Validatable.

Responses

c.JSON(http.StatusOK, payload)
c.XML(http.StatusOK, payload)         // application/xml
c.String(http.StatusOK, "hello")
c.HTML(http.StatusOK, "<h1>Hi</h1>")  // raw - sanitize user input
c.Resource(userResource)              // calls ToResource() and serializes
c.NoContent()                         // 204
c.Status(http.StatusAccepted)         // header only

// For a streamed body or full control, write to the writer directly:
c.Response.Header().Set("Content-Type", "application/octet-stream")
c.Response.Write(data)

c.JSON also sets X-Content-Type-Options: nosniff.

Serving files

File and Download serve a path resolved relative to the router’s configured FileRoot (set via SetFileRoot, defaulting to the process working directory). Containment is kernel-enforced via *os.Root, so a .. segment or a symlink escaping the root is rejected:

c.File("reports/q3.pdf")                 // inline; ServeContent
c.Download("reports/q3.pdf", "Q3.pdf")   // Content-Disposition: attachment
c.Attachment("reports/q3.pdf", "Q3.pdf") // alias for Download

Both default to Cache-Control: private, no-store (a caller-set header is preserved).

Streaming and Server-Sent Events

c.SSE writes one event:/data: frame (JSON-encoded) and flushes, setting the streaming headers and clearing the write deadline on the first call:

r.Get("/stream", func(c *router.Context) error {
    for ev := range updates {
        if err := c.SSE("update", ev); err != nil {
            return err
        }
    }
    return nil
})

For hand-rolled streams (NDJSON, custom frames), call c.PrepareStream() once and then write to c.Response directly.

Redirects

c.Redirect(http.StatusSeeOther, "/dashboard")

Redirect sanitizes the URL: relative paths are always allowed, and absolute URLs are allowed only when their host is in the router’s RedirectAllowedHosts allowlist. Everything else (cross-host URLs, protocol-relative //evil, javascript:/data: schemes, and backslash/Unicode-slash lookalikes) is rewritten to "/" to prevent open redirects. The same logic is exported as router.SanitizeRedirect.

For post-login flows, c.RedirectToIntended(fallback) issues a 303 to the safe “intended” destination stashed by the auth middleware (or the sanitized fallback). c.Intended(fallback) returns that target without redirecting. Never feed a raw ?redirect= query value to c.Redirect directly - route it through these helpers so the open-redirect sanitizer runs.

Errors

Convenience constructors return JSON-shaped errors with the standard status text or your message:

return c.NotFound()                      // {"code":404,"message":"Not Found"}
return c.BadRequest("missing field")     // {"code":400,"message":"missing field"}
return c.Unauthorized("expired token")
return c.Forbidden()
return c.Error(http.StatusConflict, "duplicate email")

For richer error handling - typed exceptions, custom renderers, dev pages - see Exceptions.

Request inspection

method := c.Method()      // GET, POST, ...
path   := c.Path()        // /users/42
ip     := c.IP()          // honors X-Forwarded-For from trusted proxies

if c.IsAjax() { /* X-Requested-With: XMLHttpRequest */ }
if c.WantsJSON() { /* Accept == application/json, or X-Inertia set */ }

Per-request storage

Pass values from middleware to handlers using Set/Get:

func auth(next router.HandlerFunc) router.HandlerFunc {
    return func(c *router.Context) error {
        user, err := authenticate(c.Request)
        if err != nil {
            return c.Unauthorized()
        }
        c.Set("user", user)
        return next(c)
    }
}

r.Get("/me", func(c *router.Context) error {
    user := c.Get("user").(*models.User)
    return c.JSON(http.StatusOK, user)
})

GetString(key) is a typed shortcut. For complex types, type-assert the result of Get.

Service accessors

When the router is attached to a Velocity app (the usual case), the context exposes the service container:

c.DB()           // contract.Database
c.Cache()        // contract.CacheManager
c.Log()          // contract.Logger
c.Queue()        // contract.QueueDriver
c.Storage()      // contract.StorageManager
c.Mail()         // contract.Mailer
c.Notification() // contract.Notifier
c.Events()       // contract.Dispatcher
c.Crypto()       // contract.Encryptor
c.Validator()    // contract.Validator
c.Exceptions()   // contract.ExceptionHandler
c.Scheduler()    // scheduler.TaskScheduler
c.Auth()         // contract.AuthManager
c.CSRF()         // contract.CSRFProtector
c.View()         // contract.ViewEngine
c.Services()     // *app.Services (the whole container)

Each accessor returns a narrow stdlib-only contract interface so the router package carries no heavy driver dependencies. They panic when the corresponding service is not configured (e.g. a raw router.New() with no Velocity app wired in). To probe the container without panicking, use c.ServicesIfSet(), which returns nil when services have not been wired.