Returning HTTP handlers from HTTP handlers in Go
I like the simplicity of Go’s net/http package. A handler receives a response writer and a request, and writes a response. That is often all I need.
But when writing application handlers, I regularly want to express something slightly different: decide what should happen, then hand off to another handler to do it. Redirect to another page. Return an error response. Delegate to an existing handler.
This small adapter makes that a little more convenient.
The adapter
The standard http.HandlerFunc has no return value. My version returns an http.Handler:
type HandlerFunc func(w http.ResponseWriter, r *http.Request) http.Handler
func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if handler := f(w, r); handler != nil {
handler.ServeHTTP(w, r)
}
}
That is the entire adapter. It calls the function and, if the function returns a handler, calls that handler with the same response writer and request.
Because it implements ServeHTTP, this custom HandlerFunc is itself a standard http.Handler. The returned handler can be anything that implements that interface: an ordinary http.HandlerFunc, another custom HandlerFunc, or an existing handler from elsewhere in the application.
Returning nil means there is nothing left to delegate to. That is useful when the function has already handled the response itself.
Returning a redirect
Here is an example from a login flow. The local login endpoint delegates to a login broker:
func (h *HttpHandler) loginForm(_ http.ResponseWriter, r *http.Request) http.Handler {
slog.DebugContext(r.Context(), "redirecting local login to broker")
return myHttp.SeeOther("/login")
}
Here, myHttp is the import alias for my HTTP helper package, which contains the adapter and response helpers shown in this post. The snippets use the standard net/http and log/slog packages.
The application handler only needs to decide where to go. The redirect helper takes care of the response:
func SeeOther(url string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("HX-Request") != "" {
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("HX-Redirect", url)
w.WriteHeader(http.StatusNoContent)
return
}
http.Redirect(w, r, url, http.StatusSeeOther)
})
}
For a regular browser request, this sends a 303 See Other redirect. For an HTMX request, it sends a 204 No Content response with an HX-Redirect header, telling HTMX to perform a full-page navigation.
That distinction matters: HTMX does not process its response headers on 3xx responses, because the browser handles those redirects. The helper keeps this detail out of every endpoint that needs the same behavior. Here, the HTMX branch is selected whenever HX-Request is non-empty.
The redirect helper itself uses the standard http.HandlerFunc. There is no reason to use the custom type when a function simply writes its response.
Registering the handler
The adapter works with a normal http.ServeMux:
mux := http.NewServeMux()
mux.Handle("GET /local/login", myHttp.HandlerFunc(h.loginForm))
The method-aware pattern above requires Go 1.22 or newer. h is the application’s *HttpHandler instance.
The important part is using Handle with the adapter. The standard mux’s HandleFunc accepts a function without a return value, so it cannot accept h.loginForm directly.
In my application, a small registration helper wraps that conversion, allowing the call site to look like this:
h.HandleFunc("GET /local/login", h.loginForm)
That HandleFunc is an application helper, rather than the standard library method. Either way, the registered value is still an ordinary http.Handler.
Composing error responses
The same approach works for errors. First, a basic handler that writes a status code and its standard text:
func Status(code int) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(code)
_, err := w.Write([]byte(http.StatusText(code)))
if err != nil {
slog.ErrorContext(r.Context(), "failed to write status text", "error", err)
}
}
}
This helper is intended for responses that allow a body, such as 400, 404, or 500. A bodyless response such as 204 needs a different handler.
Then a handler that logs an error and delegates to Status:
func Error(err error, httpStatusCode int) http.Handler {
return HandlerFunc(func(w http.ResponseWriter, r *http.Request) http.Handler {
slog.ErrorContext(r.Context(), "returning http error", "error", err)
return Status(httpStatusCode)
})
}
func InternalError(err error) http.Handler {
return Error(err, http.StatusInternalServerError)
}
Now an application handler can end an error branch with:
if err != nil {
return myHttp.InternalError(err)
}
When that branch is reached, the application handler returns an error handler. The adapter runs it, the error is logged with the request context, and the returned status handler writes the response. The client receives Internal Server Error, while the underlying error stays in the logs.
Without the adapter, I could invoke the same helper explicitly:
if err != nil {
myHttp.InternalError(err).ServeHTTP(w, r)
return
}
There is nothing wrong with that. The custom adapter just lets me combine delegation and the early return into one statement. Across handlers with several branches, I find that easier to read.
One response, one owner
There is one convention worth keeping clear: if I return a handler to produce the response, I let that handler write it.
Writing a body or committing a status code before returning a redirect or error handler would be too late for that handler to choose a different status. The adapter does not buffer or undo anything; it passes through the same ResponseWriter.
Conversely, a handler that writes the response directly can return nil:
func health(w http.ResponseWriter, r *http.Request) http.Handler {
w.WriteHeader(http.StatusNoContent)
return nil
}
For a handler this simple, I would usually keep the standard function signature instead. This example just shows what nil means: stop delegation, with no additional response written by the adapter.
A small convenience
I still use the standard http.HandlerFunc wherever it fits. This custom type is useful when a handler mostly makes decisions and delegates the resulting response to reusable helpers.
It does not introduce a separate response type or change how net/http works. It gives me a convenient way to say “return this handler”, while keeping everything compatible with the standard handler interface.
Sometimes a few lines of glue are enough to make everyday code a little nicer to write.