Responses
The second type parameter of a handler is the response body, serialized exactly as returned.
type ItemOut struct {
ID string `json:"id" doc:"The item's identifier"`
Name string `json:"name" doc:"The item's display name"`
Owner string `json:"owner,omitzero" doc:"The user the item belongs to"`
}
r.Get("/items/{item_id}", func(ctx *muzak.Context, in ItemParams) (ItemOut, error) {
return ItemOut{ID: in.ID, Name: "Foo"}, nil
})
There is no wrapper type, no response_model= and no filtering pass at run time. The
handler's return type is the response model, which means a field you did not declare
cannot leak: it is not part of the type, and the compiler is what tells you, not a bug
report.
That is why response models are kept separate from storage types:
// Item is the store's own type, deliberately separate from the response
// models. A field added here does not become visible to clients until a
// handler puts it on a response type.
type Item struct {
ID string
Name string
}
The body is encoded fully into a buffer before anything is written, so a failure part way
through encoding still produces a clean error response instead of a truncated body.
Content-Type: application/json; charset=utf-8 and Content-Length are set for you.
Status codes
A status that never changes for a route is part of the route.
r.Post("/items/", handlers.CreateItem, muzak.Status(http.StatusCreated))
A status that depends on what happened is set in the handler.
func CreateItem(ctx *muzak.Context, in schemas.ItemCreateIn) (schemas.ItemOut, error) {
store := muzak.From[*core.ItemStore](ctx)
if err := store.Create(core.Item{ID: in.ID, Name: in.Name}); err != nil {
return schemas.ItemOut{}, asHTTPError(err)
}
if in.Async {
ctx.SetStatus(http.StatusAccepted)
}
return schemas.ItemOut{ID: in.ID, Name: in.Name}, nil
}
The two are orthogonal. You never have to choose between returning a value and setting a status, and you never declare something dynamic as if it were fixed. When both are used the imperative call wins.
Details worth knowing:
- Without either, the status is
200. ctx.Status()reports what will be written, which is the route's declared default untilSetStatuschanges it.- Codes outside 100 to 599 are clamped to 500.
- Calling
SetStatusafter the body has started writing has no effect and is logged at warn level, because the status line has by then been sent. 204and304write no body at all, whatever the handler returned.
func Feed(ctx *muzak.Context, in schemas.FeedIn) (schemas.FeedOut, error) {
if !in.IfModifiedSince.IsZero() && !feedUpdatedAt.After(in.IfModifiedSince.Time) {
ctx.SetStatus(http.StatusNotModified)
return schemas.FeedOut{}, nil
}
// ...
}
Headers
ctx.SetHeader("Last-Modified", feedUpdatedAt.UTC().Format(http.TimeFormat))
ctx.SetHeader("Vary", "Save-Data, X-Tag, Cookie")
ctx.AddHeader("Vary", "Accept-Encoding") // appends rather than replacing
SetHeader replaces any value already set; AddHeader appends, which is what repeated
headers such as Set-Cookie and Vary require. Headers must be set before the handler
returns: once the response has begun, net/http ignores further changes.
Cookies
session := uuid.NewV4().String()
ctx.SetCookie(&http.Cookie{
Name: "session_id",
Value: session,
Path: "/",
// HttpOnly keeps the session out of reach of scripts, and SameSite keeps
// it off cross-site requests. Secure belongs here too once this is served
// over TLS, which is why it is named rather than omitted.
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Secure: false,
MaxAge: 3600,
})
Muzak does not modify the cookie. Setting Secure, HttpOnly and SameSite
appropriately is the caller's job, which is covered in
Cookies.
Responses with no body
muzak.Empty is also usable as an output type. The generated document then describes no
response content at all, which is the honest description of a route that answers with
nothing.
r.Delete("/items/{item_id}", func(ctx *muzak.Context, in ItemParams) (muzak.Empty, error) {
store := muzak.From[*core.ItemStore](ctx)
if _, err := store.Get(in.ID); err != nil {
return muzak.Empty{}, muzak.NotFound("Item not found")
}
return muzak.Empty{}, nil
}, muzak.Status(http.StatusNoContent))
Pair it with muzak.Status(http.StatusNoContent). At 200 an Empty value still
encodes as {}, since that is what it serializes to; at 204 nothing is written.
HTML
A handler returning muzak.HTML bypasses JSON encoding entirely. The string is written
verbatim under text/html, and the generated document describes the response as
text/html rather than as a JSON schema.
func LoginForm(ctx *muzak.Context, _ muzak.Empty) (muzak.HTML, error) {
return muzak.HTML(`<body>
<form action="/login/" method="post">
<label>Username <input name="username" autocomplete="username"></label>
<label>Password <input name="password" type="password" autocomplete="current-password"></label>
<input type="submit" value="Sign in">
</form>
</body>`), nil
}
Everything else about the route is unchanged, so its status, headers and errors work
exactly as they do for a JSON route. The value is written as given: Muzak does not escape
it, because it cannot tell markup the handler meant from text it did not. Interpolating
anything a client supplied is the handler's job to escape, with html/template or
html.EscapeString.
Documenting other outcomes
A handler that reports a failure through an error does not describe that outcome in its return type, so declare it at registration.
r.Get("/items/{item_id}", handlers.ReadItem,
muzak.WithResponseDoc(http.StatusNotFound, "The item does not exist"))
WithResponseDoc is recorded in the OpenAPI document and has no effect at runtime.
Applied to a router, it documents the response for every route beneath it, which is how
app.Include(admin, muzak.WithResponseDoc(418, "I'm a teapot")) reads. The body is
described as Muzak's error envelope, because that is what a returned error produces.
When the outcome carries a body of its own instead, name the model:
r.Post("/users/", handlers.CreateUser,
muzak.Status(http.StatusCreated),
muzak.WithResponseModel[schemas.ErrorOut](http.StatusBadRequest, "The request was malformed"))
One operation can carry a different schema at every status code that way.
Response Models covers it in full, including
inheritance, Empty and HTML models, and what reaches the document.
Writing the response yourself
For a file download or any other body Muzak should not encode, take the writer.
func Download(ctx *muzak.Context, in FileParams) (muzak.Empty, error) {
file, err := os.Open(filepath.Join(storageDir, in.Name))
if err != nil {
return muzak.Empty{}, muzak.NotFound("no such file")
}
defer file.Close()
stat, err := file.Stat()
if err != nil {
return muzak.Empty{}, err
}
ctx.SetHeader("Content-Disposition", `attachment; filename="report.pdf"`)
http.ServeContent(ctx.ResponseWriter(), ctx.Request(), stat.Name(), stat.ModTime(), file)
return muzak.Empty{}, nil
}
Writing to ctx.ResponseWriter() bypasses Muzak's response encoding: the returned value
is not serialized and SetStatus stops having an effect, so return the zero Out value
with a nil error afterwards. The writer supports http.ResponseController, so flushing
and hijacking work as usual.
For streaming a sequence of events rather than one body, reach for Server-Sent Events instead, which keeps the typed contract and the generated documentation.
Where to go next
Dependencies covers how services and resolved values reach a handler, Error Handling covers what happens when a handler returns an error, and JSON covers the encoding in detail.