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 ¶
- func BindValue[T any](w http.ResponseWriter, r *http.Request, field, kind, raw string, dst *T, ...) bool
- func BindValuePtr[T any](w http.ResponseWriter, r *http.Request, field, kind, raw string, dst **T, ...) bool
- func BindValues[T any](w http.ResponseWriter, r *http.Request, field, kind string, raw []string, ...) bool
- func CookiePresent(r *http.Request, name string) bool
- func ParseBool[T ~bool](s string) (T, error)
- func ParseFloat[T wireFloat](s string) (T, error)
- func ParseSigned[T wireSigned](s string) (T, error)
- func ParseUnsigned[T wireUnsigned](s string) (T, error)
- func RequestIDFromContext(ctx context.Context) string
- func RequirePresent(w http.ResponseWriter, r *http.Request, present bool, field, kind string) bool
- func SetDefaultValidationFailed(h ValidationFailedHandler)
- func SetGlobalJSONCodec(c JSONCodec)
- func SetHandleUnknownError(h UnknownErrorHandler)
- func WithLimits(h http.Handler, l Limits) http.Handler
- func WriteError(w http.ResponseWriter, r *http.Request, err error)
- func WriteValidationError(w http.ResponseWriter, r *http.Request, err error)
- type CORSOptions
- type Chain
- type CompressOptions
- type HealthPaths
- type JSONCodec
- type Limits
- type Logger
- type Middleware
- type Option
- type ResponseHeaderWriter
- type Server
- func (s *Server) Codec() JSONCodec
- func (s *Server) Handle(pattern string, h http.Handler, mws ...Middleware) *Server
- func (s *Server) HandleFunc(pattern string, h http.HandlerFunc) *Server
- func (s *Server) Handler() http.Handler
- func (s *Server) Logger() Logger
- func (s *Server) Mux() *http.ServeMux
- func (s *Server) RegisterHealthCheck(name string, timeout time.Duration, fn func(context.Context) error) *Server
- func (s *Server) RegisterMiddleware(name string, mw Middleware) *Server
- func (s *Server) SetCORS(opts CORSOptions) *Server
- func (s *Server) SetDefaultMaxBodySize(bytes int64) *Server
- func (s *Server) SetDefaultMaxHeaderSize(kb int) *Server
- func (s *Server) SetDefaultReadTimeout(d time.Duration) *Server
- func (s *Server) SetDefaultWriteTimeout(d time.Duration) *Server
- func (s *Server) SetHandleNotFound(h http.Handler) *Server
- func (s *Server) SetJSONCodec(c JSONCodec) *Server
- func (s *Server) SetLogger(l Logger) *Server
- func (s *Server) Start(addr string) error
- func (s *Server) Stop(ctx context.Context) error
- func (s *Server) Use(mw Middleware) *Server
- func (s *Server) With(names []string, h http.HandlerFunc) http.HandlerFunc
- type StatusError
- type UnknownErrorHandler
- type ValidationFailedHandler
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
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 ParseFloat ¶ added in v1.2.0
ParseFloat parses s as a float sized to T (float32 / float64).
func ParseSigned ¶ added in v1.2.0
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
ParseUnsigned is the unsigned counterpart of ParseSigned.
func RequestIDFromContext ¶
RequestIDFromContext returns the request ID stored by RequestID, or "".
func RequirePresent ¶ added in v1.2.0
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 SetHandleUnknownError ¶ added in v1.3.6
func SetHandleUnknownError(h UnknownErrorHandler)
SetHandleUnknownError installs a process-wide handler for service errors that are not craftgo typed errors (no HTTPStatus) - use it to map a domain error to a status, redact, or return a uniform envelope. Pass nil to revert to the default (log with trace context + 500 + message). Safe to call concurrently with dispatch; a single-pointer atomic swap keeps the hot path lock-free.
func WithLimits ¶
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 WriteError ¶ added in v1.3.6
func WriteError(w http.ResponseWriter, r *http.Request, err error)
WriteError is the indirection generated handlers call when service logic returns a non-nil error. It splits on whether the error is a recognised craftgo typed error:
- a StatusError is rendered directly from its interface — the declared HTTP status, the optional `@header`/`@cookie` writes via ResponseHeaderWriter, then a JSON body: the codec encodes the error's declared body struct, or — when the error declares no body and would marshal to `{}` — a `{code, message}` envelope built from `ErrCode()` / `Error()` so clients can still discriminate the failure. A typed error is an expected outcome (a declared 4xx/5xx), so it is NOT logged;
- anything else (a bare errors.New / fmt.Errorf) is delegated to the SetHandleUnknownError handler, whose default logs the error with the request's trace context and responds 500.
Header precedence: WriteResponseHeaders writes user-declared fields FIRST, then the framework stamps `Content-Type: application/json; charset=utf-8` LAST, so a `@header("Content-Type")` field is overridden - intentional, since the body that follows is always JSON. Use a passthrough handler for a different content type.
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.
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 ¶
HealthPaths is the override pair for `/healthz` and `/readyz`.
type JSONCodec ¶
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 ¶
Logger aliases the public Logger interface so handler-internal code does not need a second import.
type Middleware ¶
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 ResponseHeaderWriter ¶ added in v1.3.6
type ResponseHeaderWriter interface {
WriteResponseHeaders(http.ResponseWriter)
}
ResponseHeaderWriter is the optional extension a typed error implements when it declares `@header` / `@cookie` fields. WriteError calls it before the status line so those values reach the wire ahead of the JSON body.
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 ¶
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) Handle ¶
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 ¶
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) Mux ¶
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 ¶
SetDefaultMaxBodySize configures the default request body size cap.
func (*Server) SetDefaultMaxHeaderSize ¶
SetDefaultMaxHeaderSize configures the default request header size cap (in kilobytes - Go's http.Server uses a kilobyte unit internally).
func (*Server) SetDefaultReadTimeout ¶
SetDefaultReadTimeout configures the default per-method read timeout.
func (*Server) SetDefaultWriteTimeout ¶
SetDefaultWriteTimeout configures the default per-method write timeout.
func (*Server) SetHandleNotFound ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 StatusError ¶ added in v1.3.6
StatusError is the contract every craftgo-generated typed error satisfies: a standard error that also reports an HTTP status. WriteError renders these directly. Application code can implement it on its own error types to have them rendered with a chosen status instead of falling through to the unknown-error handler.
type UnknownErrorHandler ¶ added in v1.3.6
type UnknownErrorHandler func(w http.ResponseWriter, r *http.Request, err error)
UnknownErrorHandler renders an error that is NOT a recognised StatusError - a bare errors.New / fmt.Errorf returned by service logic that carries no HTTP status. It receives the request so it can read trace context for logging. The default logs the error with the request's trace IDs and responds 500 with a `{"message": ...}` body; SetHandleUnknownError swaps it process-wide.
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.