-
Release v1.0.0 Stable
released this
2026-05-29 09:48:37 -06:00 | 0 commits to main since this releasev1.0.0
code.nochebuena.dev/einherjar/web
Architecture Decisions Resolved
Decision Outcome One module vs. four separate modules One module — four sub-packages always ship together web.Newmiddleware orderRecover → RequestID → RequestLogger → CORS (outermost first) CORS auto-apply Only when AllowedOriginsis non-empty — off by defaultRate limit store pattern RateLimiterStoreinterface (Echo-style) — swap backend without changing middlewareIn-memory cleanup mechanism Background goroutine + time.Ticker(5 min eviction) — noworkerdependencyRate limit store error policy Fail-open — availability over hard enforcement last_seenmiddlewareExcluded — application-domain concern, not transport-level Request ID algorithm UUID v7 (time-ordered) with UUID v4 fallback health.CheckabledefinitionImported from contracts/observability— not redefined inhealthhttputil error handler Centralized Error(logger, w, r, err)— single log per failed request, level derived from HTTP statushttputil error log levels ≥500 → Error, 4xx → Warn, 499 → Info — mirrors Echo's centralized handler pattern httputil error code coverage 16 codes — complete coverage (adds ErrOutOfRange,ErrAborted,ErrDataLoss,ErrCancelledover micro-lib)Lifecycle error types xerrors.Unavailablefor bind failure,xerrors.Internalfor shutdown failure — logz auto-enrichesStartup log message "server: listening"emitted after goroutine launch — confirms port is bound and accepting
API
Root package
import "code.nochebuena.dev/einherjar/web" // Types type Config struct { Server server.Config AllowedOrigins []string `env:"EINHERJAR_SERVER_CORS_ORIGINS" envSeparator:","` } // Constructor func New(logger logging.Logger, cfg ...Config) server.Server // Environment // EINHERJAR_SERVER_CORS_ORIGINS=https://a.com,https://b.com — comma-separated; empty = CORS off // (all server.Config env vars also apply — see server package)serverimport "code.nochebuena.dev/einherjar/web/server" // Interface type Server interface { lifecycle.Component // OnInit, OnStart, OnStop observability.Identifiable // ModulePath, ModuleVersion — read by launcher banner chi.Router // Mount, Get, Post, Put, Delete, Patch, Use, ... } // Types type Config struct { Host string `env:"EINHERJAR_SERVER_HOST" envDefault:"0.0.0.0"` Port int `env:"EINHERJAR_SERVER_PORT" envDefault:"8080"` ReadTimeout time.Duration `env:"EINHERJAR_SERVER_READ_TIMEOUT" envDefault:"5s"` WriteTimeout time.Duration `env:"EINHERJAR_SERVER_WRITE_TIMEOUT" envDefault:"10s"` IdleTimeout time.Duration `env:"EINHERJAR_SERVER_IDLE_TIMEOUT" envDefault:"120s"` ShutdownTimeout time.Duration `env:"EINHERJAR_SERVER_SHUTDOWN_TIMEOUT" envDefault:"10s"` } type Option func(*opts) // Constructors func New(logger logging.Logger, cfg Config, opts ...Option) Server func WithMiddleware(mw ...func(http.Handler) http.Handler) Option // Lifecycle behaviour // OnInit — applies registered middleware to the router // OnStart — binds TCP listener synchronously; logs "server: listening" on success; // returns xerrors.ErrUnavailable on bind failure // OnStop — graceful http.Server.Shutdown within ShutdownTimeout; // returns xerrors.ErrInternal on unexpected shutdown failure // Environment // EINHERJAR_SERVER_HOST=0.0.0.0 — bind address (default 0.0.0.0) // EINHERJAR_SERVER_PORT=8080 — listen port (default 8080) // EINHERJAR_SERVER_READ_TIMEOUT=5s — HTTP read timeout (default 5s) // EINHERJAR_SERVER_WRITE_TIMEOUT=10s — HTTP write timeout (default 10s) // EINHERJAR_SERVER_IDLE_TIMEOUT=120s — keep-alive idle timeout (default 120s) // EINHERJAR_SERVER_SHUTDOWN_TIMEOUT=10s — graceful shutdown budget (default 10s)mwimport "code.nochebuena.dev/einherjar/web/mw" // Types type StatusRecorder struct { http.ResponseWriter Status int } func (r *StatusRecorder) WriteHeader(code int) type RateLimiterStore interface { Allow(ctx context.Context, key string) (bool, error) } type InMemoryRateLimiterStore struct { /* unexported */ } func NewInMemoryRateLimiterStore(rps float64, burst int) *InMemoryRateLimiterStore func (s *InMemoryRateLimiterStore) Allow(ctx context.Context, key string) (bool, error) // Middleware constructors func Recover() func(http.Handler) http.Handler func RequestID(generator func() string) func(http.Handler) http.Handler func RequestLogger(logger logging.Logger) func(http.Handler) http.Handler func CORS(origins []string) func(http.Handler) http.Handler func CORSAllowAll() func(http.Handler) http.Handler func IPRateLimit(store RateLimiterStore, logger logging.Logger) func(http.Handler) http.Handler func UserRateLimit(store RateLimiterStore, logger logging.Logger) func(http.Handler) http.Handlerhttputilimport "code.nochebuena.dev/einherjar/web/httputil" // Types type HandlerFunc func(w http.ResponseWriter, r *http.Request) error func (h HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) // Note: ServeHTTP writes the error response without logging (no logger in scope). // Use Handle/HandleNoBody/HandleEmpty for centralized logging, or call Error explicitly. // Generic handler adapters — centralized logging included func Handle[Req, Res any](v valid.Validator, logger logging.Logger, fn func(ctx context.Context, req Req) (Res, error)) http.HandlerFunc func HandleNoBody[Res any](logger logging.Logger, fn func(ctx context.Context) (Res, error)) http.HandlerFunc func HandleEmpty[Req any](v valid.Validator, logger logging.Logger, fn func(ctx context.Context, req Req) error) http.HandlerFunc // Centralized error handler // Logs at the level derived from HTTP status, then writes the standardized JSON body. // ≥500 → Error (logz auto-enriches with error_code and WithContext fields) // 4xx → Warn (client mistake — not a server failure) // 499 → Info (client cancelled intentionally) func Error(logger logging.Logger, w http.ResponseWriter, r *http.Request, err error) // Response helpers func JSON(w http.ResponseWriter, status int, v any) func NoContent(w http.ResponseWriter)healthimport "code.nochebuena.dev/einherjar/web/health" // Types type Config struct { CheckTimeout time.Duration `env:"EINHERJAR_HEALTH_CHECK_TIMEOUT" envDefault:"5s"` } type Response struct { Status string `json:"status"` Components map[string]ComponentStatus `json:"components"` } type ComponentStatus struct { Status string `json:"status"` Latency string `json:"latency,omitempty"` Error string `json:"error,omitempty"` } // Constructors func NewHandler(logger logging.Logger, checks ...observability.Checkable) http.Handler func NewHandlerWithConfig(logger logging.Logger, cfg Config, checks ...observability.Checkable) http.Handler // Environment // EINHERJAR_HEALTH_CHECK_TIMEOUT=5s — maximum time to wait for all checks (default 5s)
Install
go get code.nochebuena.dev/einherjar/web@v1.0.0
Dependencies
Module Version Role code.nochebuena.dev/einherjar/contractsv1.0.0 Interface definitions code.nochebuena.dev/einherjar/corev1.0.0 logz, xerrors, valid github.com/go-chi/chi/v5v5.2.1 HTTP router (embedded in Server interface) github.com/google/uuidv1.6.0 UUID v7/v4 request ID generation golang.org/x/timev0.11.0 Token bucket for InMemoryRateLimiterStoreDownloads