cf_http

package module
v0.0.9 Latest Latest
Warning

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

Go to latest
Published: Aug 17, 2026 License: Apache-2.0 Imports: 31 Imported by: 0

README

caerus-framework-http

CI codecov License

caerus-framework-http owns the HTTP serving lifecycle for Caerus services: configuration, timeouts, graceful drain, request telemetry, and common stdlib middleware. It serves any Go net/http-compatible handler.

The router and routes remain app-owned. Echo, Gin, chi, http.ServeMux, and GraphQL handlers can be registered without making this module depend on them.

Wiring

Docs: docs/wiring-and-health.md · docs/reload.md · docs/long-lived-connections.md · docs/examples.md · docs/errors.md

App-owned consumer (golden path)

Declare the HTTP chassis beside the data-plane components. The app resolves it at Init, builds its own router, composes middleware, and registers the final handler.

httpServer := cf_http.New(
    cf_http.WithConfigSource("http", "config/http.json"),
)

fw := cf.New(&cf.FrameworkOptions{
    Components: []cf.CaerusComponent{
        postgres,
        valkey,
        httpServer,
        app.New(),
    },
})
func (a *API) GetDependencies() []string {
    return []string{cf_http.ComponentName}
}

func (a *API) Init(ctx context.Context, fw *cf.CaerusFramework) error {
    server, ok := cf.Get[*cf_http.Server](fw)
    if !ok {
        return errors.New("http component missing")
    }

    mux := http.NewServeMux()
    registerRoutes(mux)
    handler := cf_http.Chain(
        cf_http.Metrics(server),
        cf_http.RequestID(),
        cf_http.Recover(a.Logger, nil),
    )(mux)
    server.SetHandler(handler)
    return nil
}

The same boundary works with Echo, Gin, chi, or a GraphQL http.Handler. Router specific route labels are supplied by the app through the documented Record or GraphQL helpers. REST series carry http_instrumentation=middleware|app (middleware = cf_http.Metrics; app = explicit Record / router shim). That is normal for REST — watch route="unknown" and double-counting, not middleware itself. See docs/wiring-and-health.md.

Simple wiring

For a one-off binary, add the component directly and resolve it with cf.MustGet after initialization.

Configuration

WithConfigSource("http", "config/http.json") self-registers the http source. The source uses the HTTP_ environment prefix and the --http file-path flag. Bind and server timeouts are restart-required; metrics enablement reloads live. restart_policy (handled default, or immediate) selects what happens when a restart-required setting changes on reload — see docs/reload.md. TLS, PROXY protocol, and forwarded-header normalization belong to the Ingress, mesh, reverse proxy, or load balancer in front of this component.

Security middleware

Optional CORS, CSRF, SecurityHeaders, and Compression middleware live in this module. They are opt-in and typed: you only get the behavior you configure.

CORS: one rule, enforced at build time

CORS has a rule like a club bouncer: "you can't say everyone is allowed (*) and bring your cookies (credentials) at the same time." Browsers reject that combination — Access-Control-Allow-Origin: * plus Access-Control-Allow-Credentials: true never works, and any server that sends both is inviting a security review.

Prefer CORSConfig.Validate() in Init (or wiring) and return the error (ErrCORSCredentialsWildcard). CORS(cfg) still panics on the same rule as a last-line construction guard — the function returns only Middleware, not (Middleware, error), so a missed Validate cannot serve a browser-rejected policy:

cfg := cf_http.CORSConfig{
    AllowCredentials: true,
    AllowedOrigins:   []string{"*"},
}
if err := cfg.Validate(); err != nil {
    return err // framework Init path
}
cors := cf_http.CORS(cfg) // panics if Validate was skipped on a bad combo

Fix it the way a correct config would look — either name your origins explicitly with credentials, or use * without credentials:

cf_http.CORS(cf_http.CORSConfig{
    AllowCredentials: true,
    AllowedOrigins:   []string{"https://app.example.com"},
})
CSRF: pick one mode

CSRF(cfg) is opt-in. Cookie-session apps need it; Authorization: Bearer APIs usually do not (the browser will not attach that header by itself).

Mode owns the whole product. Empty Mode is synchronizer. Do not set HttpOnly yourself — there is no HttpOnly field.

Mode Who it is for Cookie Unsafe POST must also send
synchronizer (default) HTML forms, or an SPA that copies a token the server put in HTML/JSON/a GET header HttpOnly (JS cannot read it) Origin/Referer and header or form field matching the cookie
double_submit SPA that reads document.cookie into X-CSRF-Token not HttpOnly Origin/Referer and matching header/form
origin_only You only want the Origin belt (strict Ingress, no token UX) none Origin/Referer only

All three fail closed when an unsafe method has neither Origin nor Referer. Origin is compared to r.Host — sit behind an edge that owns Host.

Wrong: “HttpOnly cookie, SPA copies it into the header.”
Right: synchronizer + echo the token from CSRFTokenFrom / ExposeTokenHeader,
       or double_submit (readable cookie). Those are different modes.

Call CSRFConfig.Validate() in Init and return the error (unknown Mode, ExposeTokenHeader with origin_only, or a TrustedHosts entry that looks like a URL). CSRF(cfg) panics on the same errors because it returns only Middleware:

cfg := cf_http.CSRFConfig{
    Mode:         cf_http.CSRFSynchronizer,
    TrustedHosts: []string{"api.example.com"},
}
if err := cfg.Validate(); err != nil {
    return err // framework Init path
}
csrf := cf_http.CSRF(cfg) // panics if Validate was skipped on a bad combo

Origin vs Host. Empty TrustedHosts compares Origin/Referer to r.Host. That is safe only behind an edge that sets Host (Ingress). A client can otherwise send Host: evil.example and Origin: https://evil.example and they match. Non-empty TrustedHosts is the allowlist: Origin host must be in that list (host or host:port, no scheme); r.Host is ignored.

Wrong: CSRF on a raw socket with empty TrustedHosts, trusting r.Host.
Right: TrustedHosts: []string{"api.example.com"} when nothing rewrites Host.

Synchronizer — two ways to give the page the token (construct options; default is app echo only):

App echo (CSRFTokenFrom) — always available in synchronizer / double_submit. On the GET that mints the cookie, the handler can read the token (context, not document.cookie) and put it in HTML or JSON:

mux.HandleFunc("/form", func(w http.ResponseWriter, r *http.Request) {
    token := cf_http.CSRFTokenFrom(r)
    fmt.Fprintf(w, `<input type="hidden" name="csrf_token" value="%s">`, token)
})

Response header (ExposeTokenHeader: true) — middleware sets X-CSRF-Token on GET and HEAD (not OPTIONS, so CORS preflights do not see it). A same-origin SPA reads response.headers.get("X-CSRF-Token"). Default false. Wrap the API mux only. Do not put this in front of a CDN-cached public GET (the header is a secret; a shared cache can leak it). Cross-origin fetch also needs CORS Access-Control-Expose-Headers.

cf_http.CSRF(cf_http.CSRFConfig{}) // synchronizer, app echoes via CSRFTokenFrom
cf_http.CSRF(cf_http.CSRFConfig{ExposeTokenHeader: true}) // same mode, GET header too
cf_http.CSRF(cf_http.CSRFConfig{Mode: cf_http.CSRFDoubleSubmit})
cf_http.CSRF(cf_http.CSRFConfig{Mode: cf_http.CSRFOriginOnly})

HTML forms may send csrf_token instead of the header (FormField, default csrf_token). JSON bodies are not parsed as forms. Secure defaults to true (HTTPS-only cookie). See docs/errors.md to route rejections through an ErrorWriter.

XSS on your own origin can still call your API as the user. Keep the session cookie HttpOnly regardless of CSRF mode.

Security headers

Optional SecurityHeaders(cfg) sets X-Content-Type-Options: nosniff by default (set NoSniff: false to skip). HSTS is off until HSTSMaxAge is a positive number of seconds — do not turn it on for a plain-HTTP laptop listener. Prefer SecurityHeadersConfig.Validate() in Init (negative max-age); SecurityHeaders(cfg) panics on the same error.

cf_http.SecurityHeaders(cf_http.SecurityHeadersConfig{
    HSTSMaxAge:            31536000, // one year
    HSTSIncludeSubdomains: true,
})

The edge can still send these. This helper is for apps that terminate TLS in-process or want nosniff without waiting on Ingress.

Compression
  • Compression(cfg) — gzip responses above MinSize for clients that accept gzip. Only JSON, HTML, and other text/* (not SSE). Images and octet-stream pass through. WriteHeader is delayed until the body is large enough so Content-Encoding is not applied too late. WebSocket hijacks stay untouched. BREACH: gzip can leak secrets if the same response mixes a secret (CSRF token, session fragment) with attacker-controlled text (a search query reflected in HTML). Do not compress those pages; keep secrets off gzip’d HTML/JSON that includes user input.
  • RequestLog(get) — access line with method, route, status, duration, request ID, and partial client_ip (IPv4 /24, IPv6 /48 via cf_logs.ClientIP). RequestLogWith sets omit / full or a getter for an identity the app already trusts. This module never reads X-Forwarded-For. Query, body, and cookies stay off.
  • MaxBodyBytes(n, write) — opt-in 413 when the request body is larger than n bytes (n <= 0 is a no-op). Not on the Server by default: a global limit would break uploads and large GraphQL variables. Wrap JSON POST routes (auth-api JSON POSTs should); leave multipart upload routes off or on a much larger n. Rejections go through ErrorWriter (nil = DefaultErrorWriter); use the same writer as Recover / CSRF, or problem.Write. Handlers that read the body themselves can call IsBodyTooLarge(err) after Read / Decode.
Wrong: Chain(..., MaxBodyBytes(1<<20, nil))(mux) when mux also serves
       multipart uploads.
Right: jsonMux := MaxBodyBytes(1<<20, nil)(jsonRoutes)

Telemetry

The standard middleware records request count, status class, duration, and in-flight requests. The component also reports lifecycle metrics. Applications own business metrics by implementing cf_observability.MetricsProvider.

GraphQL operation metrics are available from the optional graphql package. REST applications may use the optional problem package for RFC 9457 responses; GraphQL and OAuth handlers retain their native error envelopes.

GraphQL operation metrics

The cf_http/graphql package wraps any GraphQL-over-HTTP http.Handler (gqlgen, graph-gophers, Echo/Gin/chi frontends) and records operation-level metrics on top of the ordinary /graphql HTTP metrics.

  • Default = no operation-name metrics. Clients can invent endless operationName values; the series stay off until you opt in, and the middleware does not read or parse the request body in that mode.
  • OnlyOperations("GetUser", "ListUsers") turns named series on for that allowlist only. Generate the list from checked-in .graphql/operation files or a persisted-query map — never auto-learn it from live traffic.
  • WithOtherBucket() (optional) collapses everything outside the allowlist into one bounded other label.
  • AllOperations() measures every detected name. DANGEROUS: operation names are client-controlled, so this is a public cardinality-abuse vector — documented as an escape hatch only, not for public endpoints.
  • WithPeekWindow(n) (optional) — how many leading POST bytes may be inspected for operationName when tracking is on: omit → 8 KiB, n > 0 → peek n, 0 → full body read (costly; opt-in). Tradeoff: named series require inspecting the request; default peek bounds that cost — details in docs/graphql-metrics.md.

Emitted series (only while operation metrics are enabled): http_graphql_operations_total{operation,status_class,graphql_instrumentation}, http_graphql_operation_duration_seconds_sum/count, plus resolver series from RecordResolver. graphql_instrumentation is http_peek (auto body-peek middleware) or app (engine/explicit hooks) — filter on http_peek in dev to find leftover auto-instrumentation. Ordinary http_requests_total for the /graphql route remains in every mode.

See docs/examples.md and docs/graphql-metrics.md.

License

Apache License 2.0. See LICENSE and NOTICE.

Documentation

Overview

Package cf_http provides Caerus lifecycle and middleware support for net/http-compatible application handlers.

Index

Constants

View Source
const (
	// ErrorCodeBadRequest indicates invalid request data.
	ErrorCodeBadRequest = "BAD_REQUEST"

	// ErrorCodeUnauthorized indicates missing or invalid authentication.
	ErrorCodeUnauthorized = "UNAUTHORIZED"

	// ErrorCodeForbidden indicates insufficient permissions.
	ErrorCodeForbidden = "FORBIDDEN"

	// ErrorCodeNotFound indicates the requested resource was not found.
	ErrorCodeNotFound = "NOT_FOUND"

	// ErrorCodeConflict indicates a conflict with the current state.
	ErrorCodeConflict = "CONFLICT"

	// ErrorCodeInternal indicates an internal server error.
	ErrorCodeInternal = "INTERNAL_ERROR"

	// ErrorCodeValidation indicates validation errors.
	ErrorCodeValidation = "VALIDATION_ERROR"

	// ErrorCodePayloadTooLarge indicates the request body exceeded MaxBodyBytes.
	ErrorCodePayloadTooLarge = "PAYLOAD_TOO_LARGE"
)

Common error codes

View Source
const (
	// HTTPInstrumentationMiddleware marks samples from cf_http.Metrics middleware
	// (route from r.Pattern / "unknown"). This is the usual REST path.
	HTTPInstrumentationMiddleware = "middleware"
	// HTTPInstrumentationApp marks samples from an explicit Record call
	// (Echo/chi shims, custom middleware). Also normal — use when the router
	// does not set r.Pattern.
	HTTPInstrumentationApp = "app"
)

Values for the http_instrumentation metric label on http_requests_* series.

View Source
const (
	// ComponentName is the default framework name for the HTTP server.
	ComponentName = "http"
	// ComponentStage is the serving plane, above the data plane.
	ComponentStage = cf.Stage("app")
)
View Source
const (
	// GraphQLInstrumentationApp marks samples recorded by application /
	// engine hooks (RecordGraphQLMetric, StartOperation, RecordResolver).
	GraphQLInstrumentationApp = "app"
	// GraphQLInstrumentationHTTPPeek marks samples from graphql.Metrics
	// auto body-peek extraction — convenient, not the usual production path.
	GraphQLInstrumentationHTTPPeek = "http_peek"
)

Values for the graphql_instrumentation metric label.

Variables

View Source
var ErrCORSCredentialsWildcard = errors.New(
	"cf_http: CORS AllowCredentials cannot be true when AllowedOrigins contains '*'",
)

ErrCORSCredentialsWildcard is returned by CORSConfig.Validate when AllowCredentials is true and AllowedOrigins contains "*".

View Source
var ErrCSRFExposeTokenOriginOnly = errors.New("cf_http: ExposeTokenHeader cannot be used with origin_only")

ErrCSRFExposeTokenOriginOnly is returned when ExposeTokenHeader is set with origin_only (that mode has no token to expose).

View Source
var ErrCSRFInvalidTrustedHost = errors.New("cf_http: TrustedHosts entries must be host or host:port, not a URL")

ErrCSRFInvalidTrustedHost is returned when a TrustedHosts entry is empty or looks like a URL (scheme or path) instead of host or host:port.

View Source
var ErrCSRFUnknownMode = errors.New("cf_http: unknown CSRF Mode")

ErrCSRFUnknownMode is returned by CSRFConfig.Validate when Mode is not empty and not one of the three products.

View Source
var ErrSecurityHeadersHSTSMaxAge = errors.New("cf_http: HSTSMaxAge must be >= 0")

ErrSecurityHeadersHSTSMaxAge is returned when HSTSMaxAge is negative.

View Source
var ErrServerRestartRequired = errors.New("cf_http: server settings changed; immediate restart requested")

ErrServerRestartRequired is returned by Run when a live configuration reload changed settings that cannot rebind in place and the active restart policy was "immediate". Run has already drained and returned; the process should exit so the orchestrator starts a fresh instance with the new settings.

Functions

func CSRFTokenFrom added in v0.0.9

func CSRFTokenFrom(r *http.Request) string

CSRFTokenFrom returns the CSRF token for this request after CSRF middleware has run. On the GET that mints the cookie, the token is in context (the browser has not echoed the cookie yet). Afterwards it is also in the cookie. Empty when origin_only or CSRF did not run.

func DefaultErrorWriter

func DefaultErrorWriter(w http.ResponseWriter, r *http.Request, failure Failure)

DefaultErrorWriter is the default ErrorWriter. It writes Message when non-empty, else http.StatusText(Status), via http.Error. Used when a middleware's Write option is nil.

func IsBodyTooLarge added in v0.0.9

func IsBodyTooLarge(err error) bool

IsBodyTooLarge reports whether err is an http.MaxBytesError from MaxBodyBytes / http.MaxBytesReader. Handlers that write their own 413 (for example problem.Write) should check this after reading the body.

func Record

func Record(server *Server, route string, status int, duration time.Duration)

Record adds one completed request to the server meter with http_instrumentation="app". Empty routes are normalized to unknown so arbitrary URL paths never become metric labels.

func RequestIDFrom

func RequestIDFrom(r *http.Request) string

RequestIDFrom extracts the request ID from the request context.

Types

type Bind added in v0.0.7

type Bind []string

Bind is one or more host:port listen addresses. JSON/YAML is a string for a single listener (":9090") or an array for several (ports may differ).

func (Bind) MarshalJSON added in v0.0.7

func (b Bind) MarshalJSON() ([]byte, error)

MarshalJSON writes a string when there is one address, otherwise an array.

func (*Bind) UnmarshalJSON added in v0.0.7

func (b *Bind) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts a string or an array of strings.

func (*Bind) UnmarshalYAML added in v0.0.7

func (b *Bind) UnmarshalYAML(value *yaml.Node) error

UnmarshalYAML accepts a scalar or a sequence of host:port strings.

type CORSConfig

type CORSConfig struct {
	// AllowedOrigins is a list of origins a cross-domain request can be executed from.
	// If the special "*" value is present in the list, all origins will be allowed.
	// Default value is [] (empty), which means no origins are allowed.
	AllowedOrigins []string

	// AllowedMethods is a list of methods the client is allowed to use with
	// cross-domain requests. Default value is simple methods (GET, POST, HEAD).
	AllowedMethods []string

	// AllowedHeaders is a list of non-simple headers the client is allowed to use with
	// cross-domain requests. Default value is [] (empty).
	AllowedHeaders []string

	// ExposedHeaders indicates which headers are safe to expose to the API of a CORS
	// API specification.
	ExposedHeaders []string

	// AllowCredentials indicates whether the request can include user credentials like
	// cookies, HTTP authentication or client side SSL certificates.
	AllowCredentials bool

	// MaxAge indicates how long (in seconds) the results of a preflight request
	// can be cached. Default value is 0, which means no caching.
	MaxAge int
}

CORSConfig configures CORS middleware.

func (CORSConfig) Validate

func (cfg CORSConfig) Validate() error

Validate reports whether the CORS configuration is usable.

Returns ErrCORSCredentialsWildcard when AllowCredentials is true and any AllowedOrigin is "*". Prefer calling Validate from Init (or wiring) and returning the error; CORS itself still panics on the same rule so a missed Validate cannot serve a browser-rejected policy.

type CSRFConfig

type CSRFConfig struct {
	// Mode is the CSRF product. Empty means CSRFSynchronizer. Unknown values
	// fail Validate and panic in CSRF (same construction guard as CORS).
	Mode CSRFMode

	// CookieName is the name of the CSRF cookie. Default is "_csrf".
	// Ignored in origin_only (no cookie).
	CookieName string

	// HeaderName is the name of the CSRF request header and, when
	// ExposeTokenHeader is true, the GET/HEAD response header.
	// Default is "X-CSRF-Token".
	HeaderName string

	// FormField is the HTML form field accepted on unsafe methods when the
	// header is empty. Default is "csrf_token". Only read when Content-Type
	// is application/x-www-form-urlencoded or multipart/form-data, so JSON
	// bodies are never consumed. Ignored in origin_only.
	FormField string

	// TokenLength is the length of the generated token in bytes. Default is 32.
	TokenLength int

	// Secure controls the cookie's Secure attribute. Nil (unset) means secure:
	// the cookie is only sent over HTTPS. Set a non-nil value to override,
	// e.g. false for local development over plain HTTP.
	Secure *bool

	// SameSite is the SameSite attribute of the cookie. Default is Lax.
	SameSite http.SameSite

	// TrustedHosts is an allowlist of Origin/Referer hosts (host or
	// host:port, as in url.URL.Host — no scheme). Empty means compare to
	// r.Host, which is only safe behind an edge that owns Host (Ingress).
	// Non-empty: the Origin (or Referer) host must be in this list; r.Host
	// is ignored so a client cannot spoof Host to match a fake Origin.
	TrustedHosts []string

	// ExposeTokenHeader, when true, copies the token into HeaderName on
	// GET and HEAD responses (not OPTIONS). Default false. Use it so a
	// same-origin SPA can read response.headers instead of echoing via
	// CSRFTokenFrom. Wrap the API mux only — never a CDN-cached public GET.
	// Invalid with origin_only.
	ExposeTokenHeader bool

	// Write is the ErrorWriter used for rejected requests. Nil uses
	// DefaultErrorWriter.
	Write ErrorWriter
}

CSRFConfig configures CSRF middleware.

func (CSRFConfig) Validate added in v0.0.9

func (cfg CSRFConfig) Validate() error

Validate reports whether the CSRF configuration is usable.

type CSRFMode added in v0.0.9

type CSRFMode string

CSRFMode selects one exclusive CSRF product. Empty Mode means CSRFSynchronizer (Path B). Do not combine these with a separate HttpOnly switch — Mode owns the cookie flag and which checks run.

const (
	// CSRFSynchronizer is Path B: HttpOnly cookie plus Origin/Referer plus a
	// header or form field that matches the cookie. JavaScript cannot read the
	// cookie; the app echoes the token (CSRFTokenFrom) or sets ExposeTokenHeader.
	CSRFSynchronizer CSRFMode = "synchronizer"
	// CSRFDoubleSubmit is Path A: readable cookie (HttpOnly false). The SPA
	// copies document.cookie into the header. Origin/Referer still run.
	CSRFDoubleSubmit CSRFMode = "double_submit"
	// CSRFOriginOnly is Path C: Origin/Referer only. No CSRF cookie, no
	// header match. Do not call this double-submit.
	CSRFOriginOnly CSRFMode = "origin_only"
)

type CompressionConfig

type CompressionConfig struct {
	// MinSize is the minimum size in bytes before compression is applied.
	// Default is 1024.
	MinSize int

	// Levels is the gzip compression level. Default is gzip.DefaultCompression.
	Level int
}

CompressionConfig configures compression middleware.

type ErrorWriter

type ErrorWriter func(w http.ResponseWriter, r *http.Request, failure Failure)

ErrorWriter is a function that writes an error response to the client. It receives the HTTP response writer, the original request, and the failure. Implementations should set appropriate headers, write the status code, and never expose internal error details.

type Failure

type Failure struct {
	// Status is the HTTP status code to send to the client.
	Status int

	// Code is a stable, machine-readable error code (e.g., "NOT_FOUND").
	// It may be empty for plain transport failures.
	Code string

	// Message is a safe, human-readable error message. It must never contain
	// internal causes, stack traces, or user data.
	Message string

	// RequestID is the request ID for correlation with logs, populated from
	// RequestIDFrom(r) by middleware when present.
	RequestID string
}

Failure represents an error that occurred during request processing. It contains safe, user-facing information suitable for sending to clients. Internal error details should be logged separately, not included in Failure.

func BadRequest

func BadRequest(code, message string) Failure

BadRequest creates a Failure for HTTP 400 Bad Request.

func Conflict

func Conflict(code, message string) Failure

Conflict creates a Failure for HTTP 409 Conflict.

func Forbidden

func Forbidden(code, message string) Failure

Forbidden creates a Failure for HTTP 403 Forbidden.

func InternalError

func InternalError(code, message string) Failure

InternalError creates a Failure for HTTP 500 Internal Server Error.

func NewFailure

func NewFailure(status int, code, message string) Failure

NewFailure creates a new Failure with the given status, code, and message.

func NotFound

func NotFound(code, message string) Failure

NotFound creates a Failure for HTTP 404 Not Found.

func PayloadTooLarge added in v0.0.9

func PayloadTooLarge(code, message string) Failure

PayloadTooLarge creates a Failure for HTTP 413 Content Too Large.

func Unauthorized

func Unauthorized(code, message string) Failure

Unauthorized creates a Failure for HTTP 401 Unauthorized.

func ValidationError

func ValidationError(code, message string) Failure

ValidationError creates a Failure for HTTP 422 Unprocessable Entity.

func (Failure) WithRequestID

func (f Failure) WithRequestID(requestID string) Failure

WithRequestID sets the request ID on the failure.

type Middleware

type Middleware func(http.Handler) http.Handler

Middleware is a function that wraps an http.Handler.

func CORS

func CORS(cfg CORSConfig) Middleware

CORS returns a CORS middleware with the given configuration.

Panics if cfg.Validate() fails (credentials + "*"). Prefer CORSConfig.Validate in Init and return the error; the panic remains as a construction-time guard because CORS returns only Middleware (no error slot). See the README "Security middleware" section.

func CSRF

func CSRF(cfg CSRFConfig) Middleware

CSRF returns middleware for cfg.Mode. Prefer CSRFConfig.Validate in Init; CSRF panics if Validate fails because it returns only Middleware.

All modes fail closed on unsafe methods with neither Origin nor Referer. synchronizer (default) and double_submit also require a matching token.

func Chain

func Chain(mw ...Middleware) Middleware

Chain composes middleware in order. The first middleware is outermost.

func Compression

func Compression(cfg CompressionConfig) Middleware

Compression returns a gzip compression middleware.

func MaxBodyBytes added in v0.0.9

func MaxBodyBytes(n int64, write ErrorWriter) Middleware

MaxBodyBytes bounds the request body to n bytes. n <= 0 leaves the request unchanged (the default: this middleware is opt-in so file uploads and GraphQL variables are not surprised).

Honest clients that advertise Content-Length larger than n get 413 without the inner handler running. Clients that omit or lie about length are wrapped with http.MaxBytesReader; if the handler reads past n and does not write a response, this middleware writes 413 through write (nil → DefaultErrorWriter). Pass the same ErrorWriter you use for Recover / CSRF, or a problem.Write adapter.

Apply it on JSON POST routes, not on the whole mux:

Wrong: Chain(..., MaxBodyBytes(1<<20, nil))(mux) when mux also
       serves multipart uploads.
Right: jsonMux wrapped with MaxBodyBytes; upload routes unbounded
       or a much larger n.

func Metrics

func Metrics(server *Server) Middleware

Metrics records request metrics. Uses route pattern (Go 1.22+ ServeMux) for bounded cardinality. Falls back to "unknown" when pattern is not available.

func Recover

func Recover(get func() *slog.Logger, write ErrorWriter) Middleware

Recover recovers from panics and logs them with a stack trace. Only writes an error response if the response has not been committed yet. Uses the provided ErrorWriter, or DefaultErrorWriter when write is nil.

func RequestID

func RequestID() Middleware

RequestID generates a request ID if not present and stores it in context. Validates incoming X-Request-ID header: must be <= 256 bytes and contain only alphanumeric characters, hyphens, or underscores. Invalid values are replaced.

func RequestLog

func RequestLog(logger func() *slog.Logger) Middleware

RequestLog logs each request with method, route pattern, status, duration, request ID, and a coarsened client_ip (IPv4 /24, IPv6 /48). Use RequestLogWith to omit, log a full address, or inject a trusted identity.

func RequestLogWith added in v0.0.9

func RequestLogWith(logger func() *slog.Logger, cfg RequestLogConfig) Middleware

RequestLogWith is RequestLog with an explicit IP mode and optional identity getter. omit skips the client_ip attribute entirely.

func SecurityHeaders added in v0.0.9

func SecurityHeaders(cfg SecurityHeadersConfig) Middleware

SecurityHeaders sets nosniff and, when HSTSMaxAge > 0, HSTS. Prefer Validate in Init; SecurityHeaders panics if Validate fails.

type Option

type Option func(*options)

Option configures a Server at construction time.

func WithBind added in v0.0.7

func WithBind(addrs ...string) Option

WithBind sets one or more host:port listen addresses.

func WithConfig

func WithConfig(cfg ServerConfig) Option

WithConfig applies a static configuration snapshot.

func WithConfigSource

func WithConfigSource(name, path string, opts ...SourceOption) Option

WithConfigSource binds the server to a self-registered configuration source.

func WithIdleTimeout

func WithIdleTimeout(d time.Duration) Option

WithIdleTimeout sets the keep-alive idle timeout.

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger supplies an explicit logger for tests or embedded use.

func WithMaxHeaderBytes

func WithMaxHeaderBytes(n int) Option

WithMaxHeaderBytes sets the maximum request header size.

func WithMetricsEnabled

func WithMetricsEnabled(enabled bool) Option

WithMetricsEnabled enables or disables the component MetricsProvider.

func WithName

func WithName(name string) Option

WithName sets a custom component name for multiple HTTP instances.

func WithReadHeaderTimeout

func WithReadHeaderTimeout(d time.Duration) Option

WithReadHeaderTimeout sets the maximum header-read duration.

func WithReadTimeout

func WithReadTimeout(d time.Duration) Option

WithReadTimeout sets the maximum read duration.

func WithRestartPolicy added in v0.0.3

func WithRestartPolicy(policy string) Option

WithRestartPolicy sets what happens when a live reload changes settings that cannot rebind in place ("handled" default, or "immediate").

func WithShutdownTimeout

func WithShutdownTimeout(d time.Duration) Option

WithShutdownTimeout sets the graceful drain deadline.

func WithWriteTimeout

func WithWriteTimeout(d time.Duration) Option

WithWriteTimeout sets the maximum write duration. Zero disables the deadline.

type RequestLogConfig added in v0.0.9

type RequestLogConfig struct {
	// IP is full, partial, or omit (see cf_logs.IPMode). Empty means partial.
	IP cf_logs.IPMode
	// ClientIP returns the address to format. Nil means r.RemoteAddr.
	// cf_http never reads X-Forwarded-For; pass a getter only for an
	// identity the app already trusts.
	ClientIP func(*http.Request) string
}

RequestLogConfig is the options door for RequestLogWith. RequestLog(get) is the same as RequestLogWith(get, RequestLogConfig{}): partial client_ip from RemoteAddr. Query, body, and cookies are never logged.

type RestartPolicy

type RestartPolicy string

RestartPolicy controls behavior when server settings change during config reload.

const (
	// RestartPolicyHandled logs a warning and continues with current settings.
	// This is the default and safest option for production.
	RestartPolicyHandled RestartPolicy = "handled"

	// RestartPolicyImmediate gracefully stops the server when settings change.
	// The server must be restarted externally with new settings.
	RestartPolicyImmediate RestartPolicy = "immediate"
)

type SecurityHeadersConfig added in v0.0.9

type SecurityHeadersConfig struct {
	// HSTSMaxAge is Strict-Transport-Security max-age in seconds. 0 omits
	// the header. A common production value is 31536000 (one year).
	HSTSMaxAge int

	// HSTSIncludeSubdomains adds includeSubDomains. Ignored when HSTSMaxAge is 0.
	HSTSIncludeSubdomains bool

	// HSTSPreload adds preload. Ignored when HSTSMaxAge is 0. Only set this
	// if the site is ready for the HSTS preload list (HTTPS on all hosts,
	// includeSubDomains, long max-age).
	HSTSPreload bool

	// NoSniff controls X-Content-Type-Options: nosniff. Nil (unset) means
	// on — that is why you installed this middleware. Set false to skip.
	NoSniff *bool
}

SecurityHeadersConfig configures SecurityHeaders. Installing the middleware sets X-Content-Type-Options: nosniff unless NoSniff is explicitly false. HSTS is omitted until HSTSMaxAge > 0 (do not set HSTS on a plain-HTTP local listener unless you mean it).

func (SecurityHeadersConfig) Validate added in v0.0.9

func (cfg SecurityHeadersConfig) Validate() error

Validate reports whether SecurityHeadersConfig is usable.

type Server

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

Server owns the net/http serving lifecycle for an app-owned handler.

func New

func New(opts ...Option) *Server

New creates an inert HTTP server component. It does not bind a port.

func (*Server) Addr

func (c *Server) Addr() string

Addr returns the active listener address, or the configured address before serving. It is useful when binding to port zero in tests.

func (*Server) GetDependencies

func (c *Server) GetDependencies() []string

GetDependencies implements cf.Dependencies.

func (*Server) GetInitOrderStage

func (c *Server) GetInitOrderStage() cf.Stage

GetInitOrderStage implements cf.CaerusComponent.

func (*Server) Handler

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

Handler returns the currently registered handler.

func (*Server) Health

func (c *Server) Health(ctx context.Context) error

Health implements cf.HealthProvider. Ready only after Init, SetHandler, and a successful listen in Run. Fails again when drain starts so /readyz stops traffic before Shutdown waits on in-flight requests.

func (*Server) Init

func (c *Server) Init(ctx context.Context, fw *cf.CaerusFramework) error

Init implements cf.CaerusComponent.

func (*Server) Logger

func (c *Server) Logger() *slog.Logger

Logger returns the current logger for the server. This is useful for middleware that needs to log with the same logger.

func (*Server) Metrics

func (c *Server) Metrics() []cf_observability.Metric

Metrics implements cf_observability.MetricsProvider.

func (*Server) Name

func (c *Server) Name() string

Name implements cf.CaerusComponent.

func (*Server) OnConfigReload

func (c *Server) OnConfigReload(source string, cfg any)

OnConfigReload implements cf.ConfigReloader. Metrics enablement is live; listener settings remain active until restart based on restart policy.

func (*Server) RecordGraphQLMetric

func (c *Server) RecordGraphQLMetric(operation string, status int, duration time.Duration)

RecordGraphQLMetric records one GraphQL operation sample in the dedicated http_graphql_operations_* series with graphql_instrumentation="app". Empty operation names are normalized to "unknown". Callers must pass bounded operation names (allowlist / codegen).

func (*Server) RecordGraphQLMetricFromHTTPPeek

func (c *Server) RecordGraphQLMetricFromHTTPPeek(operation string, status int, duration time.Duration)

RecordGraphQLMetricFromHTTPPeek records a sample produced by graphql.Metrics auto body-peek extraction (graphql_instrumentation="http_peek"). Prefer RecordGraphQLMetric or engine hooks in production; use the label to find leftover auto-instrumentation in scrapes / dashboards.

func (*Server) RecordGraphQLResolverMetric

func (c *Server) RecordGraphQLResolverMetric(operation, resolver string, status int, duration time.Duration)

RecordGraphQLResolverMetric records one GraphQL resolver sample in the dedicated http_graphql_resolvers_* series with graphql_instrumentation="app". Empty operation/resolver values are normalized to "unknown". Cardinality is caller-owned: pass bounded labels (codegen / app allowlist) only.

func (*Server) RegisterConfigSources

func (c *Server) RegisterConfigSources(conf any) error

RegisterConfigSources implements cf.ConfigSourceRegistrar.

func (*Server) Run

func (c *Server) Run(ctx context.Context) error

Run implements cf.Runnable.

func (*Server) SetHandler

func (c *Server) SetHandler(handler http.Handler)

SetHandler registers the app-owned HTTP handler. It must be called before Run.

func (*Server) Shutdown

func (c *Server) Shutdown(ctx context.Context) error

Shutdown implements cf.CaerusComponent. Run performs the listener drain.

type ServerConfig

type ServerConfig struct {
	Bind                 Bind          `json:"bind,omitempty" yaml:"bind,omitempty" env:"BIND" flag:"http-bind"`
	ReadTimeoutSec       *float64      `json:"read_timeout_sec,omitempty" yaml:"read_timeout_sec,omitempty" env:"READ_TIMEOUT_SEC" flag:"http-read-timeout-sec"`
	WriteTimeoutSec      *float64      `json:"write_timeout_sec,omitempty" yaml:"write_timeout_sec,omitempty" env:"WRITE_TIMEOUT_SEC" flag:"http-write-timeout-sec"`
	IdleTimeoutSec       *float64      `json:"idle_timeout_sec,omitempty" yaml:"idle_timeout_sec,omitempty" env:"IDLE_TIMEOUT_SEC" flag:"http-idle-timeout-sec"`
	ReadHeaderTimeoutSec *float64      `` /* 147-byte string literal not displayed */
	MaxHeaderBytes       *int          `json:"max_header_bytes,omitempty" yaml:"max_header_bytes,omitempty" env:"MAX_HEADER_BYTES" flag:"http-max-header-bytes"`
	ShutdownTimeoutSec   *float64      `` /* 135-byte string literal not displayed */
	MetricsEnabled       *bool         `json:"metrics_enabled,omitempty" yaml:"metrics_enabled,omitempty" env:"METRICS_ENABLED" flag:"http-metrics-enabled"`
	RestartPolicy        RestartPolicy `json:"restart_policy,omitempty" yaml:"restart_policy,omitempty" env:"RESTART_POLICY" flag:"http-restart-policy"`
}

ServerConfig is the file/env-drivable HTTP server configuration. Pointer fields distinguish omitted values from explicit zero values.

type SourceOption

type SourceOption func(*sourceOptions)

SourceOption configures the self-registered HTTP source.

func WithSourceEnvPrefix

func WithSourceEnvPrefix(prefix string) SourceOption

WithSourceEnvPrefix overrides the environment prefix for a source.

func WithSourceFormat

func WithSourceFormat(format cf_configuration.Format) SourceOption

WithSourceFormat forces the source file format.

Directories

Path Synopsis
Package graphql provides GraphQL-over-HTTP telemetry helpers for cf_http.
Package graphql provides GraphQL-over-HTTP telemetry helpers for cf_http.
Package problem provides RFC 9457 Problem Details for HTTP APIs helper.
Package problem provides RFC 9457 Problem Details for HTTP APIs helper.

Jump to

Keyboard shortcuts

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