server

package
v1.2.0 Latest Latest
Warning

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

Go to latest
Published: Jun 2, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Package server is the thin `net/http` wrapper that craftgo's generated routes register against. It owns a `*http.ServeMux`, a middleware stack, configurable JSON codec / logger, default per-method limits, and the `Start`/`Stop` lifecycle.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func BindValue added in v1.2.0

func BindValue[T any](w http.ResponseWriter, r *http.Request, field, kind, raw string, dst *T, parse func(string) (T, error)) bool

BindValue parses raw into *dst when raw is non-empty. An absent or present-but-empty value (`?x=`) leaves *dst at its zero value. A parse failure writes a validation error and returns false so the handler returns early.

func BindValuePtr added in v1.2.0

func BindValuePtr[T any](w http.ResponseWriter, r *http.Request, field, kind, raw string, dst **T, parse func(string) (T, error)) bool

BindValuePtr is the optional (`*T`) variant: it points *dst at the parsed value, leaving it nil when raw is empty.

func BindValues added in v1.2.0

func BindValues[T any](w http.ResponseWriter, r *http.Request, field, kind string, raw []string, dst *[]T, parse func(string) (T, error)) bool

BindValues parses each element of raw into *dst (repeated `?ids=1&ids=2` or a multi-value header). One bad element fails the whole bind.

func CookiePresent added in v1.2.0

func CookiePresent(r *http.Request, name string) bool

CookiePresent reports whether the named cookie is on the request. `r.Cookie` returns http.ErrNoCookie when absent, so a nil error means present. Used by the generated handler to drive RequirePresent for a required cookie parameter.

func ParseBool added in v1.2.0

func ParseBool[T ~bool](s string) (T, error)

ParseBool parses s as a bool ("1"/"t"/"true"/... per strconv).

func ParseFloat added in v1.2.0

func ParseFloat[T wireFloat](s string) (T, error)

ParseFloat parses s as a float sized to T (float32 / float64).

func ParseSigned added in v1.2.0

func ParseSigned[T wireSigned](s string) (T, error)

ParseSigned parses s as a signed integer sized to T and converts to T, covering the builtin int kinds and int-backed scalars.

func ParseUnsigned added in v1.2.0

func ParseUnsigned[T wireUnsigned](s string) (T, error)

ParseUnsigned is the unsigned counterpart of ParseSigned.

func RequestIDFromContext

func RequestIDFromContext(ctx context.Context) string

RequestIDFromContext returns the request ID stored by RequestID, or "".

func RequirePresent added in v1.2.0

func RequirePresent(w http.ResponseWriter, r *http.Request, present bool, field, kind string) bool

RequirePresent writes a 400 and returns false when a required wire parameter's key is absent. `present` is the source-specific presence test the caller computes (url.Values.Has, a non-empty header-values slice, ...); a present-but-empty value (`?q=`) counts as present, since the value may legitimately be the empty string. Mirrors the BindValue contract: the generated handler returns early when this returns false.

func SetDefaultValidationFailed

func SetDefaultValidationFailed(h ValidationFailedHandler)

SetDefaultValidationFailed installs a process-wide handler invoked for every `req.Validate()` failure. Pass nil to revert to the default. The function is safe to call concurrently with handler dispatch; a single-pointer atomic swap keeps the hot path lock-free.

func SetGlobalJSONCodec

func SetGlobalJSONCodec(c JSONCodec)

SetGlobalJSONCodec installs c as the codec returned by JSON. Call once at startup before serving traffic; the swap itself is goroutine-safe but in-flight handlers already mid-encode keep using the codec they captured.

func WithLimits

func WithLimits(h http.Handler, l Limits) http.Handler

WithLimits returns h wrapped with the runtime guards declared in l. Zero-valued fields skip their respective wrapping so the function is a cheap pass-through when the DSL declared no limits.

Wrapping order is innermost-first: MaxBodySize wraps r.Body before the handler reads it, then Timeout wraps the whole chain so the timeout includes the body-read step.

func WriteValidationError

func WriteValidationError(w http.ResponseWriter, r *http.Request, err error)

WriteValidationError is the indirection generated handlers call. Kept exported so the codegen template can name it without reflection; not intended for application use.

Types

type CORSOptions

type CORSOptions struct {
	AllowedOrigins      []string
	AllowedMethods      []string
	AllowedHeaders      []string
	ExposedHeaders      []string
	AllowCredentials    bool
	MaxAge              time.Duration
	AllowPrivateNetwork bool
}

CORSOptions configures the CORS middleware. Most fields mirror the corresponding HTTP headers; AllowedOrigins entries may use a single leading wildcard (`https://*.example.com`) or the full wildcard `*`.

func CORSPermissive

func CORSPermissive() CORSOptions

CORSPermissive returns a development-mode preset that mirrors browser defaults for non-credentialed APIs. Not suitable for production.

func CORSStrict

func CORSStrict(origin string) CORSOptions

CORSStrict returns a production-leaning preset locked to a single origin and a small set of common headers; toggle credentials on at the call site if needed.

type Chain

type Chain []Middleware

Chain composes middlewares in outermost-first order: a chain `NewChain(A, B, C).Then(h)` yields `A(B(C(h)))`, so a request flows A → B → C → h and the response leaves in reverse.

Chains are value types — Append returns a new chain rather than mutating the receiver, so a base chain shared across routes is safe to extend per call site. Nil entries are tolerated and skipped at Then time so optional middlewares can drop into the slice without an `if mw != nil` guard at every call site.

func NewChain

func NewChain(mws ...Middleware) Chain

NewChain seeds a chain with the supplied middlewares in outermost-first order. The result is a fresh slice — mutating mws after the call does not affect the chain.

func (Chain) Append

func (c Chain) Append(mws ...Middleware) Chain

Append returns a new chain with mws added at the innermost end. The receiver is unchanged.

func (Chain) Then

func (c Chain) Then(h http.Handler) http.Handler

Then folds the chain over h, producing the wrapped handler. Iteration is reverse so the slice reads naturally outermost-first while wrapping builds from the innermost slot outwards.

func (Chain) ThenFunc

func (c Chain) ThenFunc(h http.HandlerFunc) http.Handler

ThenFunc is Then for http.HandlerFunc — saves a cast at the call site when the innermost handler is a bare function.

type CompressOptions

type CompressOptions struct {
	// MinSize is the threshold below which responses skip compression.
	// Bodies smaller than this are released uncompressed because the
	// CPU cost outweighs the wire-size win. Defaults to 1024.
	MinSize int
	// Level is the gzip / deflate compression level (1..9, or
	// gzip.DefaultCompression). Defaults to gzip.DefaultCompression.
	Level int
	// SkipTypes overrides the default list of Content-Type prefixes
	// that bypass compression (media that's already byte-compressed
	// by its own format). Pass an empty slice to compress everything.
	SkipTypes []string
}

CompressOptions tunes the response compression middleware.

type HealthPaths

type HealthPaths struct {
	Liveness  string
	Readiness string
}

HealthPaths is the override pair for `/healthz` and `/readyz`.

type JSONCodec

type JSONCodec interface {
	Encode(w io.Writer, v any) error
	Decode(r io.Reader, v any) error
}

JSONCodec is the small surface generated handlers and the access-log middleware delegate to when they need to (de)serialise JSON. The default implementation wraps `encoding/json`; production projects can substitute sonic, jsoniter, or any compatible alternative.

func JSON

func JSON() JSONCodec

JSON returns the codec currently installed via SetGlobalJSONCodec (or the stdlib default when none has been set). Generated handlers call this every time they need to (de)serialise so a runtime swap takes effect on the next request.

type Limits

type Limits struct {
	// Timeout caps the full handler lifecycle (decode body → user
	// logic → encode response) by deriving a context.WithTimeout
	// from the request and handing it to the handler. Handlers that
	// honour ctx.Done() return early on deadline; handlers that do
	// not run to completion but the response writer is detached so
	// late writes are silently dropped. Passthrough endpoints opt
	// out so streaming bodies stay intact.
	//
	// The middleware does NOT goroutine-isolate the handler the way
	// [http.TimeoutHandler] does. That stdlib helper buffers the
	// response in memory and discards any panic that fires after
	// the timeout cut-off, masking real bugs as "request timed out"
	// 503s. Running in-line keeps panics visible to the outer
	// Recovery middleware at the cost of losing the force-cancel
	// guarantee on context-deaf handlers.
	Timeout time.Duration

	// MaxBodySize caps the request body size in bytes. Wraps r.Body
	// with [http.MaxBytesReader] before the user's handler runs;
	// reads past the cap return an error, which the JSON decoder
	// surfaces as 400.
	MaxBodySize int64
}

Limits bundles the per-method runtime guards the DSL surfaces via `@timeout` and `@maxBodySize`. Zero values mean "no limit" for that dimension - the wrapper only applies a guard when the corresponding field is non-zero.

Transport-level deadlines (`http.Server.ReadTimeout`, `WriteTimeout`, `IdleTimeout`, `ReadHeaderTimeout`, `MaxHeaderBytes`) are NOT modelled here - those are server-wide concerns the user configures on the underlying http.Server directly when the stdlib defaults are insufficient. This package only owns guards that can be enforced per-handler.

type Logger

type Logger = log.Logger

Logger aliases the public Logger interface so handler-internal code does not need a second import.

type Middleware

type Middleware func(http.Handler) http.Handler

Middleware wraps an http.Handler. Order: outermost first, so the slice is applied in reverse during Start.

func AccessLog

func AccessLog(logger log.Logger) Middleware

AccessLog logs one structured line per request after the response has been written, including method, path, status, and elapsed time.

Tracing identifiers (`trace_id`, `span_id`, `request_id`) are not added explicitly - `WithContext(ctx)` extracts them from the request context. Wire `otel.HTTPMiddleware(...)` and / or `RequestID()` upstream of AccessLog to populate the context.

func BodyLimit

func BodyLimit(maxBytes int64) Middleware

BodyLimit returns a middleware that caps `r.Body` at the supplied byte size. Requests that exceed it surface as a read-side error in the downstream handler (typical clients see 413).

func Compress

func Compress(opts ...CompressOptions) Middleware

Compress returns middleware that gzip- or deflate-compresses responses when the client advertises a matching Accept-Encoding. Small bodies (< MinSize) and pre-compressed media types pass through untouched.

Vary: Accept-Encoding is added to every response so caches keep the negotiated and unnegotiated copies separate.

func Recovery

func Recovery(logger log.Logger) Middleware

Recovery converts panics inside downstream handlers into a 500 response while logging a stack trace. Always installed by Server.Start as the outermost middleware. When the panic fires AFTER the handler has already committed to a status (called WriteHeader or Write), the 500 cannot be written - net/http silently drops the second WriteHeader and the body bytes would corrupt the in-flight response. In that case the middleware logs the panic loudly and lets the connection terminate; the client sees the truncated original response and the server operator sees the stack trace.

func RequestID

func RequestID() Middleware

RequestID extracts an existing X-Request-Id header or generates a new hex string, then stores it on the context (under both this package's internal key AND pkg/log's canonical key, so log.WithContext can surface it without an import cycle) and echoes it back in the response so clients can correlate logs.

func Timeout

func Timeout(d time.Duration) Middleware

Timeout enforces an upper bound on handler execution. Streaming methods should not use this - they need write-side per-message idle limits which belong to the streaming codec, not the request lifecycle.

type Option

type Option func(*Server)

Option configures a Server at construction time.

func WithHealthPaths

func WithHealthPaths(p HealthPaths) Option

WithHealthPaths overrides the default `/healthz` and `/readyz` routes.

func WithoutDefaultHealth

func WithoutDefaultHealth() Option

WithoutDefaultHealth disables the auto-registered health endpoints.

type Server

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

Server is craftgo's runtime. Methods follow a fluent style so a project `main.go` can chain configuration calls before `Start()`.

func New

func New(_ any, opts ...Option) *Server

New returns a Server with sensible defaults: JSON codec, slog logger, `/healthz` + `/readyz` health probes, no rate-limit, no CORS. Pass any number of Option values to override.

`_` is the project's ServiceContext; it's accepted only to mirror the documented constructor signature - the runtime doesn't introspect it.

func (*Server) Codec

func (s *Server) Codec() JSONCodec

Codec exposes the active JSON codec for handlers and tooling.

func (*Server) Handle

func (s *Server) Handle(pattern string, h http.Handler, mws ...Middleware) *Server

Handle registers an http.Handler under the same Go 1.22 pattern syntax HandleFunc uses. Optional variadic middlewares wrap the handler left-to-right so the FIRST entry ends up the outermost frame — the order a reader scans matches the order a request flows through. Order chosen so:

srv.Handle("POST /x", h, Auth, RateLimit, CORS)

reads "Auth wraps RateLimit wraps CORS wraps h" — request hits Auth first, response leaves CORS last.

The variadic form keeps the route line flat regardless of chain depth, so it scans top-to-bottom in the same outermost-first order the request actually flows through.

func (*Server) HandleFunc

func (s *Server) HandleFunc(pattern string, h http.HandlerFunc) *Server

HandleFunc registers a custom route on the underlying mux using Go 1.22 pattern syntax (`"VERB /path"`).

func (*Server) Handler

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

Handler returns the fully-wrapped http.Handler: mux + every global middleware registered via Server.Use + CORS (when configured) + Recovery (always outermost). Health endpoints are wired the first time Handler is called unless WithoutDefaultHealth was set.

This is the entry point both Server.Start and tests use - wrap `httptest.NewServer(srv.Handler())` to exercise the full chain without binding a real listener.

func (*Server) Logger

func (s *Server) Logger() Logger

Logger exposes the active logger for handlers and middleware.

func (*Server) Mux

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

Mux returns the underlying `*http.ServeMux`. Generated routes call HandleFunc directly via the mux to keep the dependency surface small.

func (*Server) RegisterHealthCheck

func (s *Server) RegisterHealthCheck(name string, timeout time.Duration, fn func(context.Context) error) *Server

RegisterHealthCheck adds a named probe to `/readyz`. The function is invoked under a context with the supplied timeout; a non-nil error or timeout flips the readiness response to 503.

func (*Server) RegisterMiddleware

func (s *Server) RegisterMiddleware(name string, mw Middleware) *Server

RegisterMiddleware maps a DSL middleware name to its concrete implementation. The codegen layer can later resolve `@middlewares(Name)` against this map.

func (*Server) SetCORS

func (s *Server) SetCORS(opts CORSOptions) *Server

SetCORS attaches a CORS middleware configured by opts. Calling SetCORS twice replaces the previous configuration.

func (*Server) SetDefaultMaxBodySize

func (s *Server) SetDefaultMaxBodySize(bytes int64) *Server

SetDefaultMaxBodySize configures the default request body size cap.

func (*Server) SetDefaultMaxHeaderSize

func (s *Server) SetDefaultMaxHeaderSize(kb int) *Server

SetDefaultMaxHeaderSize configures the default request header size cap (in kilobytes - Go's http.Server uses a kilobyte unit internally).

func (*Server) SetDefaultReadTimeout

func (s *Server) SetDefaultReadTimeout(d time.Duration) *Server

SetDefaultReadTimeout configures the default per-method read timeout.

func (*Server) SetDefaultWriteTimeout

func (s *Server) SetDefaultWriteTimeout(d time.Duration) *Server

SetDefaultWriteTimeout configures the default per-method write timeout.

func (*Server) SetHandleNotFound

func (s *Server) SetHandleNotFound(h http.Handler) *Server

SetHandleNotFound installs a per-server handler invoked for requests whose path does not match any registered route. Replaces the stdlib mux's default `404 page not found` body. Pass nil to fall back to the default.

Health endpoints, static handlers, and middleware-rejected requests still bypass this handler - only routes that reach the mux without a match trigger it.

func (*Server) SetJSONCodec

func (s *Server) SetJSONCodec(c JSONCodec) *Server

SetJSONCodec swaps the JSON codec used by generated handlers, the access-log middleware, and the health endpoints. The change is process-wide via SetGlobalJSONCodec; the per-Server field is kept for callers that want to introspect via Server.Codec but the authoritative value lives on the package-level atomic.

func (*Server) SetLogger

func (s *Server) SetLogger(l Logger) *Server

SetLogger replaces the active Logger and mirrors it to the package-level log.Default so codegen-emitted logic files reach the same instance via `log.Default().WithContext(ctx)` without receiving a handle through ServiceContext.

func (*Server) Start

func (s *Server) Start(addr string) error

Start binds the server to addr and serves until Stop is called. The handler chain is built by Server.Handler so the wrapping order is identical between live serving and httptest-driven test runs.

func (*Server) Stop

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

Stop gracefully shuts down the running server. Safe to call before Start (it becomes a no-op).

func (*Server) Use

func (s *Server) Use(mw Middleware) *Server

Use appends a middleware to the chain. Outer middlewares are added first; the chain is built in reverse at Start.

func (*Server) With

func (s *Server) With(names []string, h http.HandlerFunc) http.HandlerFunc

With looks up every middleware name in the registered table and wraps h in the order given (first name = outermost). Unknown names are skipped silently so a route can declare `@middlewares(Optional)` even when the wiring isn't installed yet - the runtime will pick it up the moment RegisterMiddleware adds it.

type ValidationFailedHandler

type ValidationFailedHandler func(w http.ResponseWriter, r *http.Request, err error)

ValidationFailedHandler is the function shape every generated handler calls when `req.Validate()` returns a non-nil error. The default implementation mirrors `http.Error` - a 400 with the validator's message - and applications swap it in by calling SetDefaultValidationFailed once at startup.

Jump to

Keyboard shortcuts

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