App Logo
Integrations

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 path
  • Method: HTTP method
  • Body: Parsed request body
  • Headers: Request headers
  • Query: Query parameters
  • Request: Raw *http.Request
  • User: Authenticated user (if present)
  • ResponseStatus: Response status code
  • ResponseBody: Response body
  • ResponseHeaders: Response headers
  • ResponseCookies: Response cookies
  • Redirect: Function to perform redirects
  • Handled: 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 Handled there won't prevent execution—use it only in Before for flow control.
  • Default Behavior: If Handled is 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 Handled may 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 Before and Response hooks that would delay request handling; prefer short checks or delegate heavy work to background jobs.
  • Use Before for authoritative control flow (validation, short-circuiting); use Response for response transformation; use After for non-blocking side effects e.g. logging, metrics etc.
  • Validate input carefully in Before and return helpful error messages.
  • Handle errors gracefully in After (log, don’t return).

On this page