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))}Handler Signature
Section titled “Handler Signature”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.
Error Handling
Section titled “Error Handling”When a handler returns an error, the framework processes it through a chain of checks:
1. Context Errors
Section titled “1. Context Errors”If the error is context.Canceled or context.DeadlineExceeded, the status code is set to 499 (Client Closed Request).
2. Custom Status Codes
Section titled “2. Custom Status Codes”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}3. Handler Errors
Section titled “3. Handler Errors”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.
4. Fallback
Section titled “4. Fallback”If none of the above apply, the framework creates a Problem Details response with the error message and a 500 Internal Server Error status.
Lifecycle Hooks
Section titled “Lifecycle Hooks”Every request in Cosmos runs through a hooks-aware response writer. Hooks let you tap into the request/response lifecycle at specific points:
BeforeWriteHeader
Section titled “BeforeWriteHeader”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)}BeforeWrite
Section titled “BeforeWrite”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))})AfterResponse
Section titled “AfterResponse”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.
Response Writer
Section titled “Response Writer”The framework wraps the standard http.ResponseWriter with a hooks-aware version that:
- Fires
BeforeWriteHeaderhooks before the firstWriteHeadercall. - Fires
BeforeWritehooks before eachWritecall. - Ensures
WriteHeaderis only called once (subsequent calls are no-ops). - If
Writeis called without a precedingWriteHeader, it automatically callsWriteHeader(200)(matching standard library behavior). - Preserves
http.Flushersupport 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.
Recording Responses
Section titled “Recording Responses”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.