zen

package
v0.12.14-0...-e6bae5f Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 15, 2026 License: AGPL-3.0 Imports: 31 Imported by: 0

README

Zen

A Minimalist HTTP Library for Go

Read our blog post about why we built Zen and how it works

Zen is a lightweight, minimalistic HTTP framework for Go, designed to wrap the standard library with just enough abstraction to streamline your development process—nothing more, nothing less.

Why "Zen"?

The name "Zen" reflects the philosophy behind the framework: simplicity, clarity, and efficiency.

  • Simplicity: Focus on what matters most—handling HTTP requests and responses with minimal overhead.
  • Clarity: A thin wrapper that feels natural, staying true to Go's idiomatic style.
  • Efficiency: Built on Go's robust standard library, Zen adds no unnecessary complexity or dependencies.

Features

  • Built directly on the Go standard library (net/http).
  • Thin abstractions for routing, middleware, and error handling.
  • Support for HTTPS connections with TLS certificates.
  • Context-based graceful shutdown handling.
  • No bloat—just the essentials.
  • Fast and easy to integrate with existing Go projects.

Quickstart

package main

import (
	"context"
	"log"
	"log/slog"
	"net"
	"net/http"

	"github.com/unkeyed/unkey/pkg/zen"
	"github.com/unkeyed/unkey/pkg/logger"
	"github.com/unkeyed/unkey/pkg/zen/validation"
	"github.com/unkeyed/unkey/pkg/fault"
)

// Request struct for our create user endpoint
type CreateUserRequest struct {
	Name     string `json:"name"`
	Email    string `json:"email"`
	Password string `json:"password"`
}

// Response for successful user creation
type CreateUserResponse struct {
	ID    string `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email"`
}

func main() {

	// Create a new server
	server, err := zen.New(zen.Config{
		NodeID: "quickstart-server",
	})
	if err != nil {
		log.Fatalf("failed to create server: %v", err)
	}

	// Initialize OpenAPI validator
	// see the validation package how we pass in the openapi spec
	validator, err := validation.New()
	if err != nil {
		log.Fatalf("failed to create validator: %v", err)
	}

	// Simple hello world route
	helloRoute := zen.NewRoute("GET", "/hello", func(ctx context.Context, s *zen.Session) error {
		return s.JSON(http.StatusOK, map[string]string{
			"message": "Hello, world!",
		})
	})

	// POST endpoint with request validation and error handling
	createUserRoute := zen.NewRoute("POST", "/users", func(ctx context.Context, s *zen.Session) error {
		// Parse request body
		var req CreateUserRequest
		req, err := zen.BindBody[CreateUserRequest](s)
		if err != nil {
			return err
		}

		// Additional validation logic
		if len(req.Password) < 8 {
			return fault.New("password too short",
				fault.WithTag(fault.BAD_REQUEST),
				fault.WithDesc(
					"password must be at least 8 characters", // Internal description
					"Password must be at least 8 characters long" // User-facing message
				),
			)
		}

		// Process the request (in a real app, you'd save to database etc.)
		userID := "user_pretendthisisrandom"

		// Return response
		return s.JSON(http.StatusCreated, CreateUserResponse{
			ID:    userID,
			Name:  req.Name,
			Email: req.Email,
		})
	})

	// Register routes with middleware
	server.RegisterRoute(
		[]zen.Middleware{
			zen.WithLogging(),
			zen.WithErrorHandling(),
		},
		helloRoute,
	)

	server.RegisterRoute(
		[]zen.Middleware{
			zen.WithObservability(),
			zen.WithLogging(),
			zen.WithErrorHandling(),
			zen.WithValidation(validator),
		},
		createUserRoute,
	)

	// Start the server
	logger.Info("starting server",
		"address", ":8080",
	)

	// Create a listener
	listener, err := net.Listen("tcp", ":8080")
	if err != nil {
		log.Fatalf("failed to create listener: %v", err)
	}

	err = server.Serve(context.Background(), listener)
	if err != nil {
		logger.Error("server error", slog.String("error", err.Error()))
	}
}

Using TLS for HTTPS

To start the server with HTTPS, simply provide TLS certificate and key data:

package main

import (
	"context"
	"log"
	"net"

	"github.com/unkeyed/unkey/pkg/tls"
	"github.com/unkeyed/unkey/pkg/zen"
)

func main() {
	// Load TLS configuration from certificate and key files
	tlsConfig, err := tls.NewFromFiles("server.crt", "server.key")
	if err != nil {
		log.Fatalf("failed to load TLS configuration: %v", err)
	}

	// Create a server with TLS configuration
	server, err := zen.New(zen.Config{
		TLS: tlsConfig,
	})
	if err != nil {
		log.Fatalf("failed to create server: %v", err)
	}

	// Register routes...

	// Start the HTTPS server with context for graceful shutdown
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// Create a listener for HTTPS
	listener, err := net.Listen("tcp", ":443")
	if err != nil {
		log.Fatalf("failed to create listener: %v", err)
	}

	// Start in a goroutine so you can handle shutdown signals
	go func() {
		if err := server.Serve(ctx, listener); err != nil {
			log.Fatalf("server error: %v", err)
		}
	}()

	// Set up signal handling for graceful shutdown
	// ...

	// To shut down gracefully:
	cancel() // This will initiate graceful shutdown
}

Testing with Ephemeral Ports

For testing, you can use ephemeral ports to let the OS assign an available port automatically. This prevents port conflicts in testing environments:

import "github.com/unkeyed/unkey/pkg/listener"

// Get an available port and listener
listenerImpl, err := listener.Ephemeral()
if err != nil {
	t.Fatalf("failed to create ephemeral listener: %v", err)
}
netListener, err := listenerImpl.Listen()
if err != nil {
	t.Fatalf("failed to get listener: %v", err)
}

// Start the server
go server.Serve(ctx, netListener)

// Make requests to the server
resp, err := http.Get(fmt.Sprintf("http://%s/test", listenerImpl.Addr()))

This approach is especially useful for concurrent tests where multiple servers need to run simultaneously without conflicting ports.

Working with OpenAPI Validation

Zen works well with a schema-first approach to API design. Define your OpenAPI specification first, then use it for validation:

  1. Define your OpenAPI spec
  2. Initialize the validator with your spec
  3. Add the validation middleware to your routes
  4. Return properly tagged errors from your handlers

This approach ensures your API implementation strictly follows your API contract and provides excellent error messages to clients.

Philosophy

Zen is for developers who embrace Go's simplicity and power. By focusing only on essential abstractions, it keeps your code clean, maintainable, and in harmony with Go's design principles.

Security

Zen supports HTTPS connections with TLS configuration. We recommend using TLS in production environments to encrypt all client-server communication. The TLS implementation uses Go's standard library crypto/tls package with secure defaults (TLS 1.2+). For TLS configuration management, Zen uses the unkey/go/pkg/tls package which provides utilities for creating TLS configurations from certificates and keys.

Graceful Shutdown

Zen provides built-in support for graceful shutdown through context cancellation:

// Create a context that can be cancelled
ctx, cancel := context.WithCancel(context.Background())

// Create a listener and start the server with this context
listener, err := net.Listen("tcp", ":8080")
if err != nil {
	log.Fatalf("failed to create listener: %v", err)
}

go server.Serve(ctx, listener)

// When you need to shut down (e.g., on SIGTERM):
cancel()

// For more control over the shutdown timeout:
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer shutdownCancel()
err := server.Shutdown(shutdownCtx)

When a server's context is canceled, it will:

  1. Stop accepting new connections
  2. Complete any in-flight requests
  3. Release resources and exit gracefully

Documentation

Overview

Package zen provides a lightweight HTTP framework built on top of the standard library.

Zen is designed to add minimal abstraction over Go's net/http package while providing convenient utilities for common web service patterns. It follows Go's philosophy of simplicity and explicitness, offering just enough structure to make HTTP handlers more maintainable without obscuring the underlying functionality.

Core concepts:

Session: A request/response context that simplifies parsing requests and sending responses. Sessions are pooled and reused to reduce memory allocations.

Route: Represents an HTTP endpoint with its method, path, and handler. Routes can
be decorated with middleware.

Middleware: Functions that wrap handlers to provide cross-cutting functionality like
logging, error handling, tracing, and validation.

Server: Manages the HTTP server lifecycle and route registration.

Basic usage example:

// Initialize a new server
server, err := zen.New(zen.Config{
    NodeID: "service-1",
})
if err != nil {
    log.Fatalf("failed to create server: %v", err)
}

// Create a route with middleware
route := zen.NewRoute("GET", "/users/:id", func(s *zen.Session) error {
    id := s.Request().PathValue("id")
    user, err := userService.FindByID(s.Context(), id)
    if err != nil {
        return err
    }
    return s.JSON(http.StatusOK, user)
})

// Register route with middleware
server.RegisterRoute(
    []zen.Middleware{
        zen.WithTracing(),
        zen.WithLogging(),
        zen.WithErrorHandling(),
    },
    route,
)

// Create a listener and start the server
listener, err := net.Listen("tcp", ":8080")
if err != nil {
    log.Fatalf("failed to create listener: %v", err)
}
err = server.Serve(ctx, listener)

Zen is optimized for building maintainable, observable web services with minimal external dependencies and strong integration with standard Go libraries.

Index

Constants

View Source
const CATCHALL = ""

CATCHALL is a special method constant that indicates a route should handle all HTTP methods. When a route returns CATCHALL (empty string) from Method(), it will be registered without a method prefix, allowing it to match all HTTP methods.

View Source
const (
	// DefaultRequestTimeout is the default timeout for API requests
	DefaultRequestTimeout = 30 * time.Second
)
View Source
const MaxBodyCapture = 1 << 20 // 1 MiB

MaxBodyCapture is the maximum number of bytes captured from streaming request/response bodies for logging. Anything beyond this is silently dropped.

Variables

View Source
var ErrHijackAfterError = errors.New("hijack not allowed after error captured")

ErrHijackAfterError is returned when hijacking is attempted after an error was captured.

View Source
var ErrHijackNotSupported = errors.New("hijack not supported")

ErrHijackNotSupported is returned when the underlying ResponseWriter does not support hijacking.

View Source
var ErrPushNotSupported = errors.New("push not supported")

ErrPushNotSupported is returned when the underlying ResponseWriter does not support HTTP/2 push.

Functions

func Bearer

func Bearer(s *Session) (string, error)

Bearer extracts and validates a Bearer token from the Authorization header. It returns the token string if present and properly formatted.

If the header is missing, malformed, or contains an empty token, an appropriate error is returned with the BAD_REQUEST tag.

Example:

token, err := zen.Bearer(sess)
if err != nil {
    return err
}
// Validate the token

func BindBody

func BindBody[T any](s *Session) (T, error)

BindBody binds the request body to the given struct. If it fails, an error is returned, that you can directly return from your handler.

func IsStreamingContentType

func IsStreamingContentType(ct string) bool

IsStreamingContentType returns true for content types that use streaming and must not have their body buffered (gRPC, Connect streaming).

func NewRoute

func NewRoute(method string, path string, handleFn func(context.Context, *Session) error) *route

NewRoute creates a standard Route implementation with the specified method, path, and handler function.

Example:

route := zen.NewRoute("POST", "/api/users", func(ctx context.Context, s *zen.Session) error {
    var user User
    if err := s.BindBody(&user); err != nil {
        return err
    }
    result, err := createUser(s.Context(), user)
    if err != nil {
        return err
    }
    return s.JSON(http.StatusCreated, result)
})

func WithSession

func WithSession(ctx context.Context, session *Session) context.Context

WithSession stores a session pointer in the context, making it available to downstream handlers and packages for operations like adding headers.

This function enables patterns where middleware or handlers need to modify HTTP response headers from within cache operations or other utility functions that don't have direct access to the session. The session is stored using a private key type to prevent conflicts with other context values.

Parameters:

  • ctx: The parent context to extend with session storage
  • session: The zen session to store. Must not be nil.

Returns a new context with the session stored. The original context is not modified. The session can be retrieved later using SessionFromContext.

Usage example:

ctx = zen.WithSession(ctx, session)
// Now downstream code can access the session:
if s, ok := zen.SessionFromContext(ctx); ok {
    s.AddHeader("X-Custom", "value")
}

This is commonly used in middleware that enables functionality like cache debug headers, where cache operations need to write response headers without requiring explicit session passing through all function calls.

Types

type ApiRequestBuffer

type ApiRequestBuffer interface {
	Buffer(schema.ApiRequest)
}

ApiRequestBuffer abstracts the method used by WithMetrics to buffer API request events. *batch.BatchProcessor[schema.ApiRequest] satisfies this interface.

type Config

type Config struct {

	// TLS configuration for HTTPS connections.
	// If this is provided, the server will use HTTPS.
	TLS *tls.Config

	Flags *Flags

	// EnableH2C enables HTTP/2 cleartext (h2c) support.
	// This allows HTTP/2 connections without TLS, useful for internal services.
	EnableH2C bool

	// MaxRequestBodySize sets the maximum allowed request body size in bytes.
	// If 0 or negative, no limit is enforced. Default is 0 (no limit).
	// This helps prevent DoS attacks from excessively large request bodies.
	MaxRequestBodySize int64

	// ReadTimeout is the maximum duration for reading the entire request, including the body.
	// If 0, defaults to 10 seconds.
	ReadTimeout time.Duration

	// WriteTimeout is the maximum duration before timing out writes of the response.
	// If 0, defaults to 20 seconds.
	// For proxy services, this should be longer than any downstream timeout.
	WriteTimeout time.Duration
}

Config configures the behavior of a Server instance.

type ContextKey

type ContextKey[T any] struct {
	// contains filtered or unexported fields
}

ContextKey provides type-safe context storage using generics. It eliminates the need for type assertions and provides compile-time type safety when storing and retrieving values from context.

Example usage:

var userIDKey = zen.NewContextKey[string]("user_id")
ctx = userIDKey.WithValue(ctx, "user123")
userID, ok := userIDKey.FromContext(ctx)

func NewContextKey

func NewContextKey[T any](name string) ContextKey[T]

NewContextKey creates a new typed context key with the given name. The name is used for debugging and doesn't need to be globally unique since the key itself is used as the context key.

func (ContextKey[T]) FromContext

func (k ContextKey[T]) FromContext(ctx context.Context) (T, bool)

FromContext retrieves a value from the context using this key. Returns the value and true if found, or the zero value and false if not found.

func (ContextKey[T]) WithValue

func (k ContextKey[T]) WithValue(ctx context.Context, value T) context.Context

WithValue stores a value in the context using this key. Returns a new context with the value stored.

type ErrorCapturingWriter

type ErrorCapturingWriter struct {
	http.ResponseWriter
	// contains filtered or unexported fields
}

ErrorCapturingWriter wraps a ResponseWriter to capture proxy errors without writing them to the client. This allows errors to be returned to the middleware for consistent error handling.

This is useful when using httputil.ReverseProxy where you want to handle proxy errors in your handler instead of letting the proxy write directly to the client.

func NewErrorCapturingWriter

func NewErrorCapturingWriter(w http.ResponseWriter) *ErrorCapturingWriter

NewErrorCapturingWriter creates a new error capturing writer that wraps the given ResponseWriter.

func (*ErrorCapturingWriter) Error

func (w *ErrorCapturingWriter) Error() error

Error returns any error that was captured during proxy, or nil if no error occurred.

func (*ErrorCapturingWriter) Flush

func (w *ErrorCapturingWriter) Flush()

Flush implements http.Flusher for streaming responses. No-op when error captured (discarding response anyway). Ensures headers are written before flushing to support streaming.

func (*ErrorCapturingWriter) Hijack

Hijack implements http.Hijacker for WebSocket and connection takeover. Returns ErrHijackAfterError if an error was captured, as the connection state is undefined. Returns ErrHijackNotSupported if the underlying ResponseWriter doesn't support hijacking.

func (*ErrorCapturingWriter) Push

func (w *ErrorCapturingWriter) Push(target string, opts *http.PushOptions) error

Push implements http.Pusher for HTTP/2 server push. No-op returning ErrPushNotSupported when error captured or underlying writer doesn't support push.

func (*ErrorCapturingWriter) SetError

func (w *ErrorCapturingWriter) SetError(err error)

SetError captures an error. This is typically called by httputil.ReverseProxy's ErrorHandler.

func (*ErrorCapturingWriter) Unwrap

Unwrap returns underlying ResponseWriter for http.ResponseController.

func (*ErrorCapturingWriter) Write

func (w *ErrorCapturingWriter) Write(b []byte) (int, error)

Write implements http.ResponseWriter. If an error was captured, the body write is discarded to prevent partial responses from being sent to the client.

func (*ErrorCapturingWriter) WriteHeader

func (w *ErrorCapturingWriter) WriteHeader(statusCode int)

WriteHeader implements http.ResponseWriter. If an error was captured, the header write is discarded to prevent partial responses from being sent to the client.

type Flags

type Flags struct {
	// TestMode enables test mode, accepting certain headers from untrusted clients such as fake times for testing purposes.
	TestMode bool
}

Flags configures the behavior of a Server instance.

type HandleFunc

type HandleFunc func(ctx context.Context, sess *Session) error

HandleFunc is a function type that implements the Handler interface. It provides a convenient way to create handlers without defining new types.

type Handler

type Handler interface {
	// Handle processes an HTTP request encapsulated by the Session.
	// It should return an error if processing fails.
	Handle(ctx context.Context, sess *Session) error
}

Handler defines the interface for HTTP request handlers in the Zen framework. Implementations receive a Session and return an error if processing fails.

type InstanceInfo

type InstanceInfo struct {
	ID     string
	Region string
}

type LimitedWriter

type LimitedWriter struct {
	W io.Writer
	N int64
}

LimitedWriter wraps an io.Writer and stops writing after N bytes. Excess bytes are silently discarded — no error is returned so the TeeReader (and therefore the stream) is never interrupted.

func (*LimitedWriter) Write

func (lw *LimitedWriter) Write(p []byte) (int, error)

type LoggingOption

type LoggingOption func(*loggingConfig)

LoggingOption configures the WithLogging middleware.

func SkipPaths

func SkipPaths(prefixes ...string) LoggingOption

SkipPaths configures path prefixes that should not be logged. Any request whose path starts with one of these prefixes will skip logging entirely.

Example:

zen.WithLogging(zen.SkipPaths("/_unkey/internal/", "/health/"))

type Middleware

type Middleware func(handler HandleFunc) HandleFunc

Middleware transforms one handler into another, typically by adding behavior before and/or after the original handler executes.

Middleware is used to implement cross-cutting concerns like logging, authentication, error handling, and metrics collection.

func WithLogging

func WithLogging(opts ...LoggingOption) Middleware

WithLogging returns middleware that logs information about each request. It captures the method, path, status code, and processing time.

Example:

server.RegisterRoute(
    []zen.Middleware{zen.WithLogging(zen.SkipPaths("/_unkey/internal/", "/health/"))},
    route,
)

func WithMetrics

func WithMetrics(apiRequestBuffer ApiRequestBuffer, info InstanceInfo) Middleware

WithMetrics returns middleware that collects metrics about each request, including request counts, latencies, and status codes.

The metrics are buffered and periodically sent to an event buffer.

Example:

server.RegisterRoute(
    []zen.Middleware{zen.WithMetrics(eventBuffer, info)},
    route,
)

func WithObservability

func WithObservability() Middleware

WithObservability returns middleware that adds OpenTelemetry metrics and tracing to each request. It creates a span for the entire request lifecycle and propagates context.

If an error occurs during handling, it will be recorded in the span.

Example:

server.RegisterRoute(
    []zen.Middleware{zen.WithObservability()},
    route,
)

func WithPanicRecovery

func WithPanicRecovery() Middleware

WithPanicRecovery returns middleware that recovers from panics and converts them into appropriate HTTP error responses.

func WithTimeout

func WithTimeout(timeout time.Duration) Middleware

WithTimeout returns middleware that enforces a timeout on request processing. It differentiates between client-initiated cancellations and server-side timeouts.

Protocol upgrades (e.g. WebSocket) bypass the timeout: a hijacked connection is no longer a "request" with a meaningful deadline, and cancelling its context tears down the bidirectional tunnel mid-session.

func WithValidation

func WithValidation(validator *validation.Validator) Middleware

WithValidation returns middleware that validates incoming requests against an OpenAPI schema. Invalid requests receive a 400 Bad Request response with detailed validation errors.

Example:

validator, err := validation.New()
if err != nil {
    log.Fatalf("failed to create validator: %v", err)
}

server.RegisterRoute(
    []zen.Middleware{zen.WithValidation(validator)},
    route,
)

type Route

type Route interface {
	Handler

	// Method returns the HTTP method this route responds to (GET, POST, etc.).
	// Return CATCHALL to handle all HTTP methods.
	Method() string

	// Path returns the URL path pattern this route matches.
	Path() string
}

Route represents an HTTP endpoint with its method, path, and handler function. It encapsulates the behavior of a specific HTTP endpoint in the system.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server manages HTTP server configuration, route registration, and lifecycle. It provides connection pooling for session objects to reduce memory churn during request handling.

Server instances should be created with the New function and can be safely used by multiple goroutines.

func New

func New(config Config) (*Server, error)

New creates a new server with the provided configuration. It initializes the HTTP server and session pool with default timeouts.

The HTTP server is configured with reasonable defaults for production use: - ReadTimeout: 10 seconds - WriteTimeout: 20 seconds

Example:

server, err := zen.New(zen.Config{
    InstanceID: "api-server-1",
   ,
})
if err != nil {
    log.Fatalf("failed to initialize server: %v", err)
}

func (*Server) Flags

func (s *Server) Flags() Flags

func (*Server) Mux

func (s *Server) Mux() *http.ServeMux

Mux returns the underlying http.ServeMux. This is primarily intended for testing and advanced usage scenarios.

func (*Server) RegisterRoute

func (s *Server) RegisterRoute(middlewares []Middleware, route Route)

RegisterRoute adds an HTTP route to the server with the specified middleware chain. Routes are matched by both method and path, unless the method is CATCHALL (empty string) which matches all methods.

Middleware is applied in the order provided, with each middleware wrapping the next. The innermost handler (last to execute) is the route's handler.

Example:

server.RegisterRoute(
    []zen.Middleware{zen.WithLogging(), zen.WithErrorHandling()},
    zen.NewRoute("GET", "/health", healthCheckHandler),
)

// Catch-all route that handles all methods
server.RegisterRoute(
    []zen.Middleware{zen.WithLogging()},
    zen.NewRoute(zen.CATCHALL, "/{path...}", proxyHandler),
)

func (*Server) Serve

func (s *Server) Serve(ctx context.Context, ln net.Listener) error

Listen starts the HTTP server on the specified address. This method blocks until the server shuts down or encounters an error. Once listening, the server will not start again if Listen is called multiple times. If TLS configuration is provided, the server will use HTTPS.

The provided context is used to gracefully shut down the server when the context is canceled.

Example:

// Start server in a goroutine to allow for graceful shutdown
go func() {
    if err := server.Listen(ctx, ":8080"); err != nil {
        log.Printf("server stopped: %v", err)
    }
}()

func (*Server) Shutdown

func (s *Server) Shutdown(ctx context.Context) error

Shutdown gracefully stops the HTTP server, allowing in-flight requests to complete before returning or the context is canceled.

Example:

// Handle shutdown signal
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
    log.Printf("server shutdown error: %v", err)
}

type Session

type Session struct {

	// The workspace making the request.
	// We extract this from the root key or regular key
	// and must set it before the metrics middleware finishes.
	WorkspaceID string
	// contains filtered or unexported fields
}

Session encapsulates the state and utilities for handling a single HTTP request. It wraps the standard http.ResponseWriter and http.Request with additional functionality for parsing requests and generating responses.

Sessions are pooled and reused between requests to reduce memory allocations. References to sessions, requests, or responses should not be stored beyond the handler's execution.

A new Session is created for each request and passed to the route handler. The Session is automatically reset and returned to the pool after the request is handled.

func SessionFromContext

func SessionFromContext(ctx context.Context) (*Session, bool)

SessionFromContext retrieves the session pointer stored by WithSession.

This function allows utility packages and handlers to access the HTTP session for operations like adding response headers, reading request data, or accessing session metadata. The session is safely type-cast from the context value.

Parameters:

  • ctx: The context to search for a stored session

Returns:

  • session: The stored session pointer, or nil if no session was found
  • ok: true if a session was found, false otherwise

The boolean return follows Go conventions for optional values and allows callers to distinguish between "no session stored" and "session stored but nil". However, WithSession should never store a nil session in practice.

Usage example:

session, ok := zen.SessionFromContext(ctx)
if !ok {
    // No session available - cache debug disabled
    return
}
session.AddHeader("X-Cache-Debug", "api_by_id:150μs:FRESH")

Performance note: Context value lookup is O(depth) where depth is the number of nested context.WithValue calls. This is typically very fast (<100ns) for normal request contexts, but avoid calling this in tight loops.

func (*Session) AddHeader

func (s *Session) AddHeader(key, val string)

AddHeader adds a key-value pair to the response headers. This method can be called multiple times with the same key to add multiple values for the same header.

func (*Session) AuthorizedWorkspaceID

func (s *Session) AuthorizedWorkspaceID() string

AuthorizedWorkspaceID returns the workspace ID associated with the authenticated request. This is populated by authentication middleware.

Returns an empty string if no authenticated workspace ID is available.

func (*Session) BindBody

func (s *Session) BindBody(dst any) error

BindBody parses the request body as JSON into the provided destination struct. The destination must be a pointer to a struct.

If parsing fails, an appropriate error is returned. The original request body is stored in the session for potential reuse or logging.

Example:

var user User
if err := sess.BindBody(&user); err != nil {
    return err
}
// Use the parsed user data

func (*Session) BindQuery

func (s *Session) BindQuery(dst interface{}) error

BindQuery parses URL query parameters into the provided destination struct. The destination must be a pointer to a struct with json tags that match the query parameter names.

Example:

var params struct {
    Limit  int    `json:"limit"`
    Cursor string `json:"cursor"`
    Filter string `json:"filter"`
}
if err := sess.BindQuery(&params); err != nil {
    return err
}
// Use params.Limit, params.Cursor, and params.Filter

func (*Session) DisableClickHouseLogging

func (s *Session) DisableClickHouseLogging()

DisableClickHouseLogging prevents this request from being logged to ClickHouse. By default, all requests are logged to ClickHouse unless explicitly disabled.

This is useful for internal endpoints like health checks, OpenAPI specs, or requests that should not appear in analytics.

func (*Session) HTML

func (s *Session) HTML(status int, body []byte) error

HTML sends an HTML response with the given status code.

func (*Session) Init

func (s *Session) Init(w http.ResponseWriter, r *http.Request, maxBodySize int64) error

func (*Session) InternalError

func (s *Session) InternalError() string

InternalError returns the stored internal error message for logging.

func (*Session) JSON

func (s *Session) JSON(status int, body any) error

JSON sets the response status code and sends a JSON-encoded response. It automatically sets the Content-Type header to application/json.

The body is marshaled using github.com/bytedance/sonic If marshaling fails, an error is returned.

Example:

return sess.JSON(http.StatusOK, map[string]interface{}{
    "user": user,
    "token": token,
})

func (*Session) Location

func (s *Session) Location() string

Location returns the client's IP address, checking X-Forwarded-For header first, then falling back to RemoteAddr. Ports are stripped from the returned IP.

func (*Session) Plain

func (s *Session) Plain(status int, body []byte) error

Plain sends a plain text response with the given status code.

func (*Session) ProblemJSON

func (s *Session) ProblemJSON(status int, body any) error

ProblemJSON sends a JSON-encoded error response with content negotiation. If the client's Accept header includes "application/problem+json", that content type is used per RFC 9457. Otherwise it falls back to "application/json" for backwards compatibility with existing SDKs.

func (*Session) Request

func (s *Session) Request() *http.Request

Request returns the underlying http.Request. This allows direct access to the standard library request features.

Note: The returned request should not be stored across requests or modified after the handler returns.

func (*Session) RequestID

func (s *Session) RequestID() string

RequestID returns the request id for this session.

func (*Session) ResponseWriter

func (s *Session) ResponseWriter() http.ResponseWriter

ResponseWriter returns the http.ResponseWriter with status code capturing. This allows direct access to the standard library response features.

Direct manipulation of the ResponseWriter should be avoided when possible in favor of using the Session's response methods like JSON or Send.

func (*Session) Send

func (s *Session) Send(status int, body []byte) error

Send sets the response status code and sends raw bytes as the response body. This method is useful for non-JSON responses like binary data or plain text.

Unlike [JSON], this method does not set any Content-Type header automatically.

func (*Session) SetInternalError

func (s *Session) SetInternalError(err string)

SetInternalError stores the internal error message for logging purposes. This should be called by error handling middleware before converting errors to HTTP responses.

func (*Session) SetResponseBody

func (s *Session) SetResponseBody(body []byte)

SetResponseBody allows proxy handlers to feed a captured response body back into the session so zen middleware (WithLogging, WithMetrics) can log it. This is necessary for proxied responses where the body is written directly to the ResponseWriter, bypassing Session.send().

func (*Session) ShouldLogRequestToClickHouse

func (s *Session) ShouldLogRequestToClickHouse() bool

ShouldLogRequestToClickHouse returns whether this request should be logged to ClickHouse. Returns true by default, false only if explicitly disabled.

func (*Session) StatusCode

func (s *Session) StatusCode() int

StatusCode returns the HTTP status code that was written to the response. Returns 200 if no status code has been explicitly set.

func (*Session) UserAgent

func (s *Session) UserAgent() string

Directories

Path Synopsis
Package metrics provides Prometheus metric collectors for monitoring application performance.
Package metrics provides Prometheus metric collectors for monitoring application performance.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL