Bodies#
c.String(http.StatusOK, "pong")
c.HTML(http.StatusOK, "<h1>Trusted markup only</h1>")
c.JSON(http.StatusOK, web.Map{"ok": true})
c.Blob(http.StatusOK, "image/png", png)
c.Stream(http.StatusOK, "text/csv", reader)
c.NoContent() // 204Every body helper takes the status code, sets Content-Type and writes the body. Once the status line is sent it cannot change, which is why handlers should return errors instead of writing partial responses.
JSON helpers for APIs#
| Helper | Status | Notes |
|---|---|---|
c.JSON(status, v) | any | encoding/json |
c.Created(location, v) | 201 | Sets Location when non-empty |
c.Accepted(v) | 202 | Async work |
c.NoContent() | 204 | |
c.Problem(status, title, detail) | 4xx/5xx | application/problem+json (RFC 9457) |
func (p *ProductController) Store(c *web.Ctx) error {
product := p.create(c)
return c.Created(c.MustURL("products.show", "product", product.ID), product)
}Redirects#
return c.Redirect("/posts") // 302 for GET/HEAD, 303 after POST/PUT/…
return c.RedirectStatus(http.StatusMovedPermanently, "/new")
return c.RedirectRoute("posts.show", "post", 42)
return c.Back("/") // same-origin Referer, else fallback- After unsafe methods
Redirectuses 303 See Other, so the browser follows withGET. - htmx requests receive
HX-Location(navigation without a full reload) instead of a 3xx. Backignores off-site referers (open-redirect protection).web.SafeRedirect(r, target, fallback)applies the same check to any user-supplied target, such as a?next=parameter.
Files#
assets := os.DirFS("storage/reports")
return c.File(assets, "2026/q3.pdf") // Range and If-Modified-Since
return c.Download(assets, "2026/q3.pdf", "Q3 report.pdf") // Content-Disposition: attachmentMissing files and directories return 404, without a Content-Disposition header. The download name is sanitized like export file names: only its last path element is kept, control and bidi characters are removed, and non-ASCII names get an RFC 5987 filename*. An empty name uses the file's own name. Open user-controlled paths through an os.Root (root.FS()) so .. cannot escape the directory.
Headers and cookies#
c.SetHeader("Cache-Control", "no-store")
c.SetCookie(&http.Cookie{Name: "theme", Value: "dark", Path: "/", HttpOnly: true, Secure: true, SameSite: http.SameSiteLaxMode})Conditional requests#
func (pc *PostController) Show(c *web.Ctx) error {
post, err := pc.find(c)
if err != nil {
return err
}
etag, err := web.ETagOf(post) // strong ETag from the JSON encoding
if err != nil {
return err
}
if c.NotModified(etag, post.UpdatedAt) { // sets ETag/Last-Modified, writes 304 when fresh
return nil
}
return c.JSON(http.StatusOK, post)
}For optimistic concurrency on writes, c.CheckIfMatch(currentETag) returns errs.ErrPreconditionFailed (412) when the client's If-Match does not match.
Views and checked JSON#
c.Render,c.Fragmentandc.Viewrender HTML: see Views.web.JSONWith(c, status, schema, v)checks outgoing JSON against azschema: see Validation.