Skip to content

The basics

Responses

Write text, HTML, JSON, files and redirects from handlers, with safe redirect helpers.

Last updated:

Bodies#

Go
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() // 204

Every 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#

HelperStatusNotes
c.JSON(status, v)anyencoding/json
c.Created(location, v)201Sets Location when non-empty
c.Accepted(v)202Async work
c.NoContent()204
c.Problem(status, title, detail)4xx/5xxapplication/problem+json (RFC 9457)
Go
func (p *ProductController) Store(c *web.Ctx) error {
	product := p.create(c)
	return c.Created(c.MustURL("products.show", "product", product.ID), product)
}

Redirects#

Go
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 Redirect uses 303 See Other, so the browser follows with GET.
  • htmx requests receive HX-Location (navigation without a full reload) instead of a 3xx.
  • Back ignores 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#

Go
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: attachment

Missing 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#

Go
c.SetHeader("Cache-Control", "no-store")
c.SetCookie(&http.Cookie{Name: "theme", Value: "dark", Path: "/", HttpOnly: true, Secure: true, SameSite: http.SameSiteLaxMode})

Conditional requests#

Go
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.Fragment and c.View render HTML: see Views.
  • web.JSONWith(c, status, schema, v) checks outgoing JSON against a z schema: see Validation.