server

package module
v0.2.1 Latest Latest
Warning

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

Go to latest
Published: Jul 11, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

README

http/server

Quality http/server health Go Reference GitHub Tag License

A small chi wrapper

github.com/toaweme/http/server wraps net/http.Server behind a chi-backed Router and a {Name, Start, Stop} lifecycle, and bundles the middleware, params, JSON helpers, and Server-Sent Events you reach for on every service. chi stays an implementation detail - handlers never import it. It is the server half of github.com/toaweme/http and depends only on go-chi/chi.

Install

go get github.com/toaweme/http/server

Module

  • server.NewRouter() builds a [Router]; register handlers with Get/Post/Put/Delete/Patch (or Handle), nest with Group, and add middleware with Use/With.
  • server.NewServer(Config, *Router, Logger, ...Option) wraps the router in a *net/http.Server; Start blocks until Stop(ctx) shuts it down gracefully.
  • server.Param, server.Wildcard, server.RoutePattern read path data without exposing chi to handlers.
  • server.SlogMiddleware(SlogConfig, Logger) logs every request; server.AuthMiddleware(ClaimsExtractor, Logger) enforces Bearer auth and injects claims.
  • server.WriteJSON / WriteError / WriteBadRequest / ReadJSON / ReadRawJSON are the request/response helpers.
  • sse.NewHub() (sub-package server/sse) broadcasts Server-Sent Events to subscribers.

Overview

Routing

Router is chi underneath, with method helpers, groups, and scoped or inline middleware. Handlers use plain net/http signatures.

r := server.NewRouter()
r.Use(server.SlogMiddleware(server.SlogConfig{}, logger)) // root middleware

r.Get("/health", func(w http.ResponseWriter, _ *http.Request) {
	server.WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
})

r.Group("/api", func(api *server.Router) {
	api.Use(server.AuthMiddleware(extractClaims, logger)) // scoped to /api
	api.Get("/items/{id}", func(w http.ResponseWriter, req *http.Request) {
		server.WriteJSON(w, http.StatusOK, map[string]string{"id": server.Param(req, "id")})
	})
})

r.Get("/files/*", func(w http.ResponseWriter, req *http.Request) {
	_ = server.Wildcard(req) // everything after /files/
})

Use panics if called after a route is registered on the same scope (a chi guard), so add middleware first. With(mw...) returns a sub-router for one-off inline middleware. LogRoutes(logger) walks and logs every registered route.

Server lifecycle

NewServer builds the underlying *http.Server eagerly with a sane ReadHeaderTimeout default (Slowloris protection). Start serves and blocks; Stop shuts down gracefully within the context deadline. The type implements the {Name, Start, Stop} service contract.

srv := server.NewServer(server.Config{Host: "127.0.0.1", Port: 8080}, r, logger)

go func() {
	if err := srv.Start(); err != nil {
		log.Fatal(err)
	}
}()

// ... on shutdown signal:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = srv.Stop(ctx)
Configuring the underlying server

Config only holds the listen address. Everything else is set with functional options, or by mutating the raw *http.Server:

srv := server.NewServer(cfg, r, logger,
	server.WithReadHeaderTimeout(5*time.Second),
	server.WithReadTimeout(10*time.Second),
	server.WithWriteTimeout(10*time.Second),
	server.WithIdleTimeout(120*time.Second),
)

srv.HTTP().TLSConfig = tlsCfg // escape hatch for anything no Option covers

Options run after the defaults, so they override; WithReadHeaderTimeout(0) disables the timeout. Mutate HTTP() before calling Start.

Auth middleware

AuthMiddleware pulls the Bearer token, runs your ClaimsExtractor to parse it, and injects org/user/scopes into the request context (or aborts with 401). Read them back in handlers:

extract := func(token string) (*server.Claims, error) {
	return &server.Claims{OrgID: "org-1", UserID: "user-1", Scopes: []string{"read"}}, nil
}
r.Use(server.AuthMiddleware(extract, logger))

// inside a handler:
org, _ := server.OrgIDFromContext(req.Context())
user, _ := server.UserIDFromContext(req.Context())
scopes := server.ScopesFromContext(req.Context())

The Authorizer interface and ContextWith* helpers are available for building authorization checks on top.

Request logging

SlogMiddleware logs method, url, duration, and status for every request. Opt in to capturing headers and bodies (with a size cap) for local debugging:

r.Use(server.SlogMiddleware(server.SlogConfig{
	LogRequestBody:  true,
	LogResponseBody: true,
	MaxBodyBytes:    4096, // 0 means no cap
}, logger))
JSON helpers
var in CreateItem
if err := server.ReadJSON(req, &in); err != nil {
	server.WriteBadRequest(w, err)
	return
}
server.WriteJSON(w, http.StatusCreated, Item{ID: "1"})
server.WriteError(w, http.StatusNotFound, errors.New("not found"))
Server-Sent Events

The server/sse sub-package provides an SSE Writer and a topic-scoped Hub. ServeStream ties a hub topic to a handler, with heartbeats and slow-subscriber handling:

hub := sse.NewHub()

r.Get("/stream", func(w http.ResponseWriter, req *http.Request) {
	_ = sse.ServeStream(w, req, hub, "updates") // blocks until the client disconnects
})

// from anywhere:
hub.Publish("updates", sse.Event{Type: "tick", Data: "hello"})

Subscribers that fall behind have their channel closed rather than blocking the producer; Subscribers(topic) reports the current count. For one-off writing, sse.NewWriter(w) returns a *Writer with Start, Write(Event), and Ping.

Features

  • chi-backed router - Get/Post/Put/Delete/Patch/Handle, Group nesting, Use/With middleware, and LogRoutes, without leaking chi into handlers.
  • Server lifecycle - Name/Start/Stop over net/http.Server with graceful shutdown.
  • Configurable transport - WithReadHeaderTimeout/WithReadTimeout/WithWriteTimeout/WithIdleTimeout options plus a HTTP() escape hatch; secure ReadHeaderTimeout by default.
  • Param access - Param, Wildcard, RoutePattern.
  • Auth middleware - Bearer-token extraction into request context via a pluggable ClaimsExtractor; Claims, Authorizer, and *FromContext / ContextWith* helpers.
  • Request logging - structured method/url/duration/status, with optional headers and size-capped bodies.
  • JSON helpers - WriteJSON, WriteError, WriteBadRequest, ReadJSON, ReadRawJSON.
  • SSE hub - Writer for well-formed events, Hub for topic fan-out with slow-subscriber drop, and ServeStream with heartbeats.
  • Injectable logger - a minimal Logger interface defined locally so the server module never depends on the client; satisfied structurally by github.com/toaweme/log.

Runnable examples

The package tests double as usage references: router_test.go, server_test.go, auth_middleware_test.go, and sse/sse_test.go.

go test ./...

Hosted code and health reports

Reports for this repo are hosted by our code viewer, which also serves the badges and cards above.

http/server health http/server code


Made with ❤️ in Lithuania 🇱🇹.

Documentation

Overview

Package server provides the HTTP server, router, middleware, and auth primitives.

Index

Constants

This section is empty.

Variables

View Source
var ErrUnauthorized = errors.New("unauthorized")

ErrUnauthorized is returned by Authorizer.Authorize when the caller lacks permission. Map to HTTP 403 in handlers.

Functions

func AuthMiddleware

func AuthMiddleware(extract ClaimsExtractor, logger Logger) func(http.Handler) http.Handler

AuthMiddleware extracts the Bearer token, runs extract to parse it into Claims, injects OrgID/UserID/Scopes into the request context, and aborts with HTTP 401 on missing/invalid header or extractor failure.

func ContextWithOrgID

func ContextWithOrgID(ctx context.Context, orgID string) context.Context

ContextWithOrgID returns a copy of ctx carrying the org ID.

func ContextWithScopes

func ContextWithScopes(ctx context.Context, scopes []string) context.Context

ContextWithScopes returns a copy of ctx carrying the caller's scopes.

func ContextWithUserID

func ContextWithUserID(ctx context.Context, userID string) context.Context

ContextWithUserID returns a copy of ctx carrying the user ID.

func OrgIDFromContext

func OrgIDFromContext(ctx context.Context) (string, bool)

OrgIDFromContext reads the org ID set by ContextWithOrgID. ok is false if unset.

func Param

func Param(r *http.Request, key string) string

Param returns the named path parameter from the matched route, or "" if absent. Handlers should use this instead of importing chi directly.

func ReadJSON

func ReadJSON(r *http.Request, v any) error

ReadJSON decodes the request body into v.

func ReadRawJSON

func ReadRawJSON(r *http.Request) (json.RawMessage, error)

ReadRawJSON returns the request body as a json.RawMessage after validating it is well-formed JSON.

func Register

func Register(r HandleRouter, routes []Route)

Register registers all routes on r.

func RoutePattern

func RoutePattern(r *http.Request) string

RoutePattern returns the matched route pattern (e.g. "/items/{id}"), useful for metrics and structured logging. Returns "" when called outside a matched route.

func ScopesFromContext

func ScopesFromContext(ctx context.Context) []string

ScopesFromContext reads the scopes set by ContextWithScopes. Returns nil if unset.

func SlogMiddleware

func SlogMiddleware(cfg SlogConfig, logger Logger) func(http.Handler) http.Handler

SlogMiddleware logs method, url and duration for every request, plus optionally the request and response bodies when the config opts in.

func UserIDFromContext

func UserIDFromContext(ctx context.Context) (string, bool)

UserIDFromContext reads the user ID set by ContextWithUserID. ok is false if unset.

func Wildcard

func Wildcard(r *http.Request) string

Wildcard returns the value matched by a trailing "/*" catch-all, or "" if the route had no wildcard.

func WriteBadRequest

func WriteBadRequest(w http.ResponseWriter, err error)

WriteBadRequest writes err with HTTP 400.

func WriteError

func WriteError(w http.ResponseWriter, status int, err error)

WriteError writes err as a JSON ErrorResponse with the given status code. Callers map domain errors to status codes themselves.

func WriteJSON

func WriteJSON(w http.ResponseWriter, status int, v any)

WriteJSON encodes v as JSON with the given status code.

Types

type Action

type Action string

Action names the operation being checked by an Authorizer.

const (
	ActionRead   Action = "read"
	ActionWrite  Action = "write"
	ActionDelete Action = "delete"
	ActionAdmin  Action = "admin"
)

The set of actions an Authorizer can be asked to check.

type Authorizer

type Authorizer interface {
	Authorize(ctx context.Context, action Action, resourceID string) error
}

Authorizer is called before privileged operations. Return nil to allow, ErrUnauthorized to deny, or any other error to signal an infrastructure failure.

resourceID identifies the target resource; it may be empty for collection-level operations. Read identity from ctx via OrgIDFromContext / UserIDFromContext.

type Claims

type Claims struct {
	OrgID  string
	UserID string
	Scopes []string
}

Claims holds the identity fields extracted from a Bearer token.

type ClaimsExtractor

type ClaimsExtractor func(token string) (*Claims, error)

ClaimsExtractor parses a raw Bearer token and returns the claims. Return a non-nil error to reject the request with HTTP 401.

type Config

type Config struct {
	Host string
	Port int
}

Config configures a Server's listen address. Anything beyond the address (timeouts, TLS, connection hooks) is set via Option or by mutating the underlying server returned by HTTP.

type ErrorResponse

type ErrorResponse struct {
	Error string `json:"error"`
}

ErrorResponse is the canonical JSON error body.

type HandleRouter

type HandleRouter interface {
	Handle(method, pattern string, handler http.Handler)
}

HandleRouter is the minimal interface satisfied by any HTTP framework that can register a method+pattern+handler triple. *Router satisfies it.

type Logger

type Logger interface {
	Trace(msg string, args ...any)
	Debug(msg string, args ...any)
	Info(msg string, args ...any)
	Warn(msg string, args ...any)
	Error(msg string, args ...any)
}

Logger is the minimal leveled logging surface the server writes to. It is satisfied structurally by github.com/toaweme/log's Slog, so callers can inject that directly, or a null logger to discard output. It is defined here rather than imported from the client module so the server module never depends on its parent; a single concrete logger satisfies both.

type Option

type Option func(*http.Server)

Option mutates the underlying *http.Server during construction. Options run after the defaults (Addr, Handler, ReadHeaderTimeout) are applied, so they can override anything.

func WithIdleTimeout

func WithIdleTimeout(d time.Duration) Option

WithIdleTimeout sets how long to keep a keep-alive connection idle.

func WithReadHeaderTimeout

func WithReadHeaderTimeout(d time.Duration) Option

WithReadHeaderTimeout sets how long the server waits for request headers. Pass 0 to disable it (not recommended - exposes the server to Slowloris).

func WithReadTimeout

func WithReadTimeout(d time.Duration) Option

WithReadTimeout sets the maximum duration for reading an entire request.

func WithWriteTimeout

func WithWriteTimeout(d time.Duration) Option

WithWriteTimeout sets the maximum duration before timing out response writes.

type Route

type Route struct {
	Method  string
	Pattern string
	Handler http.Handler
}

Route is a single HTTP route definition. Pattern uses net/http 1.22+ placeholder syntax: {name}.

type Router

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

Router is the chi-backed request router exposed by this package.

func NewRouter

func NewRouter() *Router

NewRouter builds an empty Router.

func (*Router) Delete

func (r *Router) Delete(p string, h http.HandlerFunc)

Delete registers h for DELETE requests to pattern p.

func (*Router) Get

func (r *Router) Get(p string, h http.HandlerFunc)

Get registers h for GET requests to pattern p.

func (*Router) Group

func (r *Router) Group(prefix string, fn func(*Router))

Group creates a sub-router rooted at prefix. Middleware added inside fn applies only to routes registered on the sub-router.

func (*Router) Handle

func (r *Router) Handle(method, pattern string, h http.Handler)

Handle registers handler for method+pattern. Pattern uses chi's syntax, which mirrors stdlib's {name} placeholders for single segments and adds a trailing /* for catch-all.

func (*Router) LogRoutes

func (r *Router) LogRoutes(logger Logger)

LogRoutes walks every registered route and logs its method and pattern.

func (*Router) Patch

func (r *Router) Patch(p string, h http.HandlerFunc)

Patch registers h for PATCH requests to pattern p.

func (*Router) Post

func (r *Router) Post(p string, h http.HandlerFunc)

Post registers h for POST requests to pattern p.

func (*Router) Put

func (r *Router) Put(p string, h http.HandlerFunc)

Put registers h for PUT requests to pattern p.

func (*Router) ServeHTTP

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request)

func (*Router) Use

func (r *Router) Use(mw ...func(http.Handler) http.Handler)

Use appends middleware to this router's scope. chi panics if called after any route is registered on this scope.

func (*Router) With

func (r *Router) With(mw ...func(http.Handler) http.Handler) *Router

With returns a sub-router with additional inline middleware.

type Server

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

Server wraps net/http.Server around a *Router. Implements the {Name, Start, Stop} contract expected by go-shared/service.Service.

func NewServer

func NewServer(cfg Config, router *Router, logger Logger, opts ...Option) *Server

NewServer wires a Server around the router. A github.com/toaweme/log logger can be injected directly, or a null logger to discard output. Pass Options to tune the underlying *http.Server, or reach for HTTP to set fields no Option covers.

func (*Server) HTTP

func (s *Server) HTTP() *http.Server

HTTP returns the underlying *http.Server for callers that need to set fields no Option covers (TLS config, connection state hooks, error log, ...). Mutate it before calling Start; changes after the server is serving have no effect.

func (*Server) Name

func (s *Server) Name() string

Name identifies the service in a service registry.

func (*Server) Start

func (s *Server) Start() error

Start serves until Stop is called. It blocks and returns nil on a clean shutdown.

func (*Server) Stop

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

Stop gracefully shuts the server down, respecting ctx's deadline.

type SlogConfig

type SlogConfig struct {
	// LogRequestBody captures and logs the request body.
	LogRequestBody bool
	// LogResponseBody captures and logs the response body.
	LogResponseBody bool
	// LogRequestHeaders logs incoming request headers.
	LogRequestHeaders bool
	// LogResponseHeaders logs outgoing response headers (captured at end of request).
	LogResponseHeaders bool
	// MaxBodyBytes caps how much of each body is captured. 0 means no cap.
	MaxBodyBytes int
}

SlogConfig controls what the SlogMiddleware emits on each request. LogRequestBody and LogResponseBody are off by default — they're useful for local debugging but should stay off in prod (PII, large payloads, perf).

Directories

Path Synopsis
Package sse provides a small Server-Sent Events writer and broadcast Hub.
Package sse provides a small Server-Sent Events writer and broadcast Hub.

Jump to

Keyboard shortcuts

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