Controllers

Redirects, Metadata, And Errors

Set response metadata, redirect safely, and return expected HTTP errors.

By Guillermo Alvarez - Published - Updated

Redirect after changes

Use controller redirects from actions:

func (c *PostsController) Create(
    _ http.ResponseWriter,
    _ *http.Request,
) error {
    post := c.posts.Create()
    return c.RedirectTo("/posts/"+post.Param, http.StatusSeeOther)
}

RedirectTo writes the redirect response and skips automatic rendering.

Redirect back safely

Use RedirectBackOrTo when a form should return to the referring page:

return c.RedirectBackOrTo("/posts", http.StatusSeeOther)

GoLazy only uses same-host referrers. External referrers fall back to the path you provide.

Set status and headers

Response metadata can be set before rendering:

c.Status(http.StatusCreated)
c.Header().Set("Cache-Control", "no-store")
c.Set("post", post)
return nil

Status does not commit the response early, so the normal template render can still run.

Return expected errors

Wrap expected failures with an HTTP status:

if !ok {
    return lazycontroller.Error(
        http.StatusNotFound,
        fmt.Errorf("post %q not found", postID),
    )
}

The framework error handler uses the status code and renders app/views/app/error.html.tpl through the current layout. If the application does not provide that view, lazyapp falls back to the framework's mobile friendly default error view. Use ReturnFile or ServeErrorPage only when an action deliberately wants to serve a static public status page. Raw error details are passed to the template only in development/detail mode. Production responses get the status code and status text without exposing err.Error() or backtrace frames.

Add application backtraces

Use lazyerrors.New when application code wants an unexpected error to carry the source location where it was returned:

post, err := c.posts.Get(postID)
if err != nil {
    return lazyerrors.New("load post %q: %w", postID, err)
}

The formatted error is prefixed with the caller, such as posts.PostsController.Show: load post "hello": not found. %w wrapping keeps working with errors.Is and errors.As; multiple %w values expose Unwrap() []error.

Inspect a recorded trace through a local interface:

type backtracer interface {
    Backtrace() []lazyerrors.Frame
}

var traced backtracer
if errors.As(err, &traced) {
    for _, frame := range traced.Backtrace() {
        log.Println(frame)
    }
}

Each frame exposes Function, File, and Line, and still implements String() for compact log output. Recovered panics use the same backtrace shape, so panic detail pages render through the same frame list.

Show detailed errors

When an application runs through lazy, the build uses the lazydev tag and unexpected errors, lazyerrors backtraces, and recovered panic backtraces are shown with detail in the rendered error view. Production builds hide details unless the app opts in:

lazyapp.New(lazyapp.Config{
    ForceDetailErrors: true,
})

The built-in detailed error view displays frame paths relative to the active Go workspace or current directory when possible. Module-cache frames are shortened to module-relative paths such as golang.org/toolchain@version/src/net/http/server.go. In lazydev, clicking a frame sends a fire-and-forget request to /_golazy/open-editor; GoLazy starts $EDITOR with the recorded absolute file path and line. VS Code-style editors receive -g file:line. Terminal editors such as vim, nvim, and nano are opened in a new terminal when GoLazy can discover one from Terminal.app on macOS, $TERMINAL, Linux default terminal commands, parent-process terminal names, or common terminal executables.

Unexpected errors are also logged to stderr so production responses can stay brief without losing the operational error.