Endpoint Hooks
Configure endpoint hooks to customize request and response handling in GoBetterAuth.
Overview
Endpoint hooks provide a flexible way to inject custom logic into the request and response lifecycle of your authentication endpoints. The enhanced middleware supports three distinct hooks:
- Before Hook: Runs before the handler. Modify the request, set custom headers/cookies, or short-circuit with a custom response.
- Response Hook: Intercepts and transforms the handler’s response. Use for content-type conversion, custom formatting, or error handling.
- After Hook: Runs asynchronously after the response is sent. Ideal for logging, metrics, or background tasks.
Hooks are injected via config, keeping your business logic decoupled from delivery concerns. All errors and important events are logged using the slog package for consistent, human-readable output.
Example Use Cases
- Add authentication/authorization checks before handlers.
- Transform API responses (e.g. send HTML instead of JSON).
- Log requests and responses for auditing.
- Inject custom headers or cookies.
Handler Control and Middleware Flexibility
Handlers retain full control over the HTTP response lifecycle. The EndpointHooksMiddleware wraps handlers but passes the original http.ResponseWriter and *http.Request to them, so users can still perform all HTTP operations (such as redirects, setting headers, writing custom responses, etc.) directly within their handlers.
func MyHandler(w http.ResponseWriter, r *http.Request) {
// Redirect example
if someCondition {
http.Redirect(w, r, "/new-path", http.StatusFound)
return
}
// Custom response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"message": "Success"}`))
}The middleware's hooks (Before, Response, After) add interception points but do not restrict handler functionality. Hooks can modify or short-circuit requests/responses, but handlers can override or complement them as needed. This keeps the design flexible and composable.
This release makes it easier to build robust, extensible APIs with clear separation of concerns and improved observability.
Hook Context
Each hook receives an EndpointHookContext with structured details about the request and response:
Path: Endpoint pathMethod: HTTP methodBody: Parsed request bodyHeaders: Request headersQuery: Query parametersRequest: Raw*http.RequestUser: Authenticated user (if present)ResponseStatus: Response status codeResponseBody: Response bodyResponseHeaders: Response headersResponseCookies: Response cookiesRedirect: Function to perform redirectsHandled: bool
See the EndpointHooksConfig for the rest of the type definitions.
Before Hook
The Before hook executes before the main handler. Use it to:
- Validate or normalize payloads
- Block or short-circuit requests (supports custom responses)
- Set custom headers/cookies
- Enforce rate limits or feature flags
If Before returns an error or a custom response, request processing is aborted and the response is sent immediately. Keep Before fast and non-blocking.
Short-Circuiting with ctx.Handled
The Handled field in EndpointHookContext is a boolean flag that signals to the middleware to stop further processing of the request after the current hook. It's primarily used in the Before hook to short-circuit the request flow, preventing the handler chain (including Response and After hooks) from executing. This is useful for early exits like redirects, authentication failures, or custom responses.
When using ctx.Redirect, Handled is set automatically, so there's no need to set it manually. For other scenarios, such as setting response properties directly, you must set ctx.Handled = true to prevent further processing.
Example: Early Exit with Redirect
func MyBeforeHook(ctx *models.EndpointHookContext) error {
if ctx.User == nil {
ctx.Redirect("/login", http.StatusFound)
return nil
}
return nil
}Example: Authentication/Authorization Failures
If the user lacks permissions or the session is invalid, send an error response and stop.
Before: func(ctx *gobetterauthmodels.EndpointHookContext) error {
if !hasPermission(ctx.User, ctx.Path) {
ctx.ResponseHeaders["Content-Type"] = []string{"application/json"}
ctx.ResponseStatus = http.StatusForbidden
ctx.ResponseBody = []byte(`{"message": "Access denied"}`)
ctx.Handled = true // Short-circuit to prevent further processing
return nil
}
return nil
}Other Use Cases
- Custom Early Responses: For rate limiting, maintenance mode, or API versioning mismatches, where you want to respond immediately without hitting the main handler.
- Request Modification Without Proceeding: If you modify the context but decide not to continue (rare, but possible for conditional logic).
Notes on Handled
- Not Applicable in Response/After Hooks: These run after the handler, so setting
Handledthere won't prevent execution—use it only inBeforefor flow control. - Default Behavior: If
Handledis false (default), the middleware continues to the next handler. - Error Handling: If a hook returns an error, the middleware handles it (e.g., sends a JSON error), and
Handledmay not be checked—use errors for hook-specific failures.
This keeps the middleware flexible and efficient, avoiding unnecessary processing.
Example: Custom Validation and Header
import (
"fmt"
"strings"
gobetterauthconfig "github.com/GoBetterAuth/go-better-auth/config"
gobetterauthmodels "github.com/GoBetterAuth/go-better-auth/models"
)
config := gobetterauthconfig.NewConfig(
gobetterauthconfig.WithEndpointHooks(
gobetterauthmodels.EndpointHooksConfig{
Before: func(ctx *gobetterauthmodels.EndpointHookContext) error {
if ctx.Path == "/api/auth/sign-up/email" {
if ctx.Method != "POST" {
return fmt.Errorf("only POST is allowed for %s", ctx.Path)
}
email, ok := ctx.Body["email"].(string)
if !ok {
return fmt.Errorf("email is required in request body")
}
if !strings.HasSuffix(email, "@gmail.com") {
// Short-circuit with custom error response
ctx.ResponseHeaders["Content-Type"] = []string{"application/json"}
ctx.ResponseStatus = http.StatusForbidden
ctx.ResponseBody = []byte(`{"message": "Only @gmail.com email addresses are allowed to sign up."}`)
ctx.Handled = true
return nil
}
}
// Set custom headers if you want
ctx.ResponseHeaders["X-Custom-Header"] = []string{"CustomValue"}
return nil
},
},
),
)Response Hook
The Response hook intercepts and transforms the handler’s response. Use cases include:
- Content-type conversion (e.g. JSON to HTML)
- Custom formatting
- Error handling
- Injecting headers/cookies
Example
import (
"fmt"
gobetterauthconfig "github.com/GoBetterAuth/go-better-auth/config"
gobetterauthmodels "github.com/GoBetterAuth/go-better-auth/models"
)
config := gobetterauthconfig.NewConfig(
gobetterauthconfig.WithEndpointHooks(
gobetterauthmodels.EndpointHooksConfig{
Response: func(ctx *gobetterauthmodels.EndpointHookContext) error {
if ctx.Path == "/auth/me" && ctx.ResponseStatus == 200 {
// Transform JSON response to HTML
ctx.ResponseHeaders["Content-Type"] = []string{"text/html"}
ctx.ResponseBody = []byte(fmt.Sprintf(`<html><body><h1>Data:</h1><pre>%s</pre></body></html>`, string(ctx.ResponseBody)))
}
return nil
},
},
),
)After Hook
The After hook runs asynchronously after the response is sent. Use for:
- Structured logging (all errors/events use
slog) - Auditing and metrics
- Notifications or cleanup tasks
Errors in After are logged, not returned to the client.
Example
import (
"log/slog"
gobetterauthconfig "github.com/GoBetterAuth/go-better-auth/config"
gobetterauthmodels "github.com/GoBetterAuth/go-better-auth/models"
)
config := gobetterauthconfig.NewConfig(
gobetterauthconfig.WithEndpointHooks(
gobetterauthmodels.EndpointHooksConfig{
After: func(ctx *gobetterauthmodels.EndpointHookContext) error {
userID := "unknown"
if ctx.User != nil {
userID = ctx.User.ID
}
slog.Info("Endpoint called", "user", userID, "path", ctx.Path, "status", ctx.ResponseStatus)
return nil
},
},
),
)Best Practices
- Keep hooks lightweight and fast; they run on every matching request.
- Avoid blocking I/O in
BeforeandResponsehooks that would delay request handling; prefer short checks or delegate heavy work to background jobs. - Use
Beforefor authoritative control flow (validation, short-circuiting); useResponsefor response transformation; useAfterfor non-blocking side effects e.g. logging, metrics etc. - Validate input carefully in
Beforeand return helpful error messages. - Handle errors gracefully in
After(log, don’t return).
