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
- Variables
- func Bearer(s *Session) (string, error)
- func BindBody[T any](s *Session) (T, error)
- func IsStreamingContentType(ct string) bool
- func NewRoute(method string, path string, handleFn func(context.Context, *Session) error) *route
- func WithSession(ctx context.Context, session *Session) context.Context
- type ApiRequestBuffer
- type Config
- type ContextKey
- type ErrorCapturingWriter
- func (w *ErrorCapturingWriter) Error() error
- func (w *ErrorCapturingWriter) Flush()
- func (w *ErrorCapturingWriter) Hijack() (net.Conn, *bufio.ReadWriter, error)
- func (w *ErrorCapturingWriter) Push(target string, opts *http.PushOptions) error
- func (w *ErrorCapturingWriter) SetError(err error)
- func (w *ErrorCapturingWriter) Unwrap() http.ResponseWriter
- func (w *ErrorCapturingWriter) Write(b []byte) (int, error)
- func (w *ErrorCapturingWriter) WriteHeader(statusCode int)
- type Flags
- type HandleFunc
- type Handler
- type InstanceInfo
- type LimitedWriter
- type LoggingOption
- type Middleware
- func WithLogging(opts ...LoggingOption) Middleware
- func WithMetrics(apiRequestBuffer ApiRequestBuffer, info InstanceInfo) Middleware
- func WithObservability() Middleware
- func WithPanicRecovery() Middleware
- func WithTimeout(timeout time.Duration) Middleware
- func WithValidation(validator *validation.Validator) Middleware
- type Route
- type Server
- type Session
- func (s *Session) AddHeader(key, val string)
- func (s *Session) AuthorizedWorkspaceID() string
- func (s *Session) BindBody(dst any) error
- func (s *Session) BindQuery(dst interface{}) error
- func (s *Session) DisableClickHouseLogging()
- func (s *Session) HTML(status int, body []byte) error
- func (s *Session) Init(w http.ResponseWriter, r *http.Request, maxBodySize int64) error
- func (s *Session) InternalError() string
- func (s *Session) JSON(status int, body any) error
- func (s *Session) Location() string
- func (s *Session) Plain(status int, body []byte) error
- func (s *Session) ProblemJSON(status int, body any) error
- func (s *Session) Request() *http.Request
- func (s *Session) RequestID() string
- func (s *Session) ResponseWriter() http.ResponseWriter
- func (s *Session) Send(status int, body []byte) error
- func (s *Session) SetInternalError(err string)
- func (s *Session) SetResponseBody(body []byte)
- func (s *Session) ShouldLogRequestToClickHouse() bool
- func (s *Session) StatusCode() int
- func (s *Session) UserAgent() string
Constants ¶
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.
const ( // DefaultRequestTimeout is the default timeout for API requests DefaultRequestTimeout = 30 * time.Second )
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 ¶
var ErrHijackAfterError = errors.New("hijack not allowed after error captured")
ErrHijackAfterError is returned when hijacking is attempted after an error was captured.
var ErrHijackNotSupported = errors.New("hijack not supported")
ErrHijackNotSupported is returned when the underlying ResponseWriter does not support hijacking.
var ErrPushNotSupported = errors.New("push not supported")
ErrPushNotSupported is returned when the underlying ResponseWriter does not support HTTP/2 push.
Functions ¶
func Bearer ¶
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 ¶
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 ¶
IsStreamingContentType returns true for content types that use streaming and must not have their body buffered (gRPC, Connect streaming).
func NewRoute ¶
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 ¶
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.
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 ¶
func (w *ErrorCapturingWriter) Hijack() (net.Conn, *bufio.ReadWriter, error)
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 ¶
func (w *ErrorCapturingWriter) Unwrap() http.ResponseWriter
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 ¶
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 LimitedWriter ¶
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.
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 ¶
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) Mux ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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(¶ms); 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) InternalError ¶
InternalError returns the stored internal error message for logging.
func (*Session) JSON ¶
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 ¶
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) ProblemJSON ¶
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 ¶
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) 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 ¶
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 ¶
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 ¶
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 ¶
ShouldLogRequestToClickHouse returns whether this request should be logged to ClickHouse. Returns true by default, false only if explicitly disabled.
func (*Session) StatusCode ¶
StatusCode returns the HTTP status code that was written to the response. Returns 200 if no status code has been explicitly set.
Source Files
¶
- auth.go
- context.go
- doc.go
- handler.go
- instance.go
- middleware.go
- middleware_errors.go
- middleware_logger.go
- middleware_metrics.go
- middleware_observability.go
- middleware_openapi_validation.go
- middleware_panic_recovery.go
- middleware_timeout.go
- redact.go
- request_util.go
- route.go
- server.go
- session.go
- writer_error.go
- writer_status.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Package metrics provides Prometheus metric collectors for monitoring application performance.
|
Package metrics provides Prometheus metric collectors for monitoring application performance. |