Skip to content

Handlers

λ Cosmos handlers differ from Go’s standard http.HandlerFunc in one important way: they return an error. This enables centralized error handling and cleaner control flow.

type Handler func(w http.ResponseWriter, r *http.Request) error
type HTTPStatus interface {
HTTPStatus() int
}
type Hooks interface {
AfterResponse(callbacks ...func(err error))
BeforeWrite(callbacks ...func(w http.ResponseWriter, content []byte))
BeforeWriteHeader(callbacks ...func(w http.ResponseWriter, status int))
}

A handler receives the standard http.ResponseWriter and *http.Request, and returns nil on success or an error on failure:

func getUser(w http.ResponseWriter, r *http.Request) error {
id := r.PathValue("id")
user, err := db.FindUser(r.Context(), id)
if err != nil {
return err
}
return response.JSON(w, http.StatusOK, user)
}

When a handler returns nil, the framework checks if a response was written. If not, it automatically sends a 204 No Content.

When a handler returns an error, the framework processes it through a chain of checks:

If the error is context.Canceled or context.DeadlineExceeded, the status code is set to 499 (Client Closed Request).

If the error implements the HTTPStatus interface, that status code is used:

type HTTPStatus interface {
HTTPStatus() int
}

For example:

type NotFoundError struct {
Resource string
}
func (e NotFoundError) Error() string {
return fmt.Sprintf("resource not found: %s", e.Resource)
}
func (e NotFoundError) HTTPStatus() int {
return http.StatusNotFound
}

If the error implements http.Handler (like problem.Problem), the framework calls its ServeHTTP method directly. This is how Problem Details responses are generated — see Problem Details.

If none of the above apply, the framework creates a Problem Details response with the error message and a 500 Internal Server Error status.

Every request in Cosmos runs through a hooks-aware response writer. Hooks let you tap into the request/response lifecycle at specific points:

Runs just before the status code is written to the response. Useful for capturing the actual status code:

func myHandler(w http.ResponseWriter, r *http.Request) error {
hooks := request.Hooks(r)
hooks.BeforeWriteHeader(func(w http.ResponseWriter, status int) {
// status is the HTTP status code about to be written
metrics.RecordStatus(status)
})
return response.JSON(w, http.StatusOK, data)
}

Runs just before response body bytes are written:

hooks.BeforeWrite(func(w http.ResponseWriter, content []byte) {
// content is the byte slice about to be written
metrics.RecordResponseSize(len(content))
})

Runs after the entire response is complete (including error handling). Receives the handler’s error (or nil):

hooks.AfterResponse(func(err error) {
if err != nil {
logger.Error("request failed", "err", err)
}
metrics.RecordRequestDuration(time.Since(start))
})

Hooks are registered through the request context using request.Hooks(r). The framework automatically injects the hooks context — no middleware setup is needed.

Multiple hooks of the same type can be registered. They execute in reverse registration order (last registered runs first), which mirrors how middleware wrapping works.

The framework wraps the standard http.ResponseWriter with a hooks-aware version that:

  • Fires BeforeWriteHeader hooks before the first WriteHeader call.
  • Fires BeforeWrite hooks before each Write call.
  • Ensures WriteHeader is only called once (subsequent calls are no-ops).
  • If Write is called without a preceding WriteHeader, it automatically calls WriteHeader(200) (matching standard library behavior).
  • Preserves http.Flusher support for streaming responses.

The wrapped writer also tracks whether WriteHeader was called. If the handler returns without writing anything, the framework sends 204 No Content.

For testing, use the Record method to execute a handler and capture the response:

req := httptest.NewRequest("GET", "/users/1", nil)
res := framework.Handler(getUser).Record(req)
if res.StatusCode != http.StatusOK {
t.Errorf("expected 200, got %d", res.StatusCode)
}

This uses httptest.NewRecorder under the hood, running the full handler lifecycle including hooks and error handling.