cf_http

package module
v0.0.4 Latest Latest
Warning

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

Go to latest
Published: Aug 14, 2026 License: Apache-2.0 Imports: 28 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. Address 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, 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 and compression
  • CSRF(cfg) — double-submit cookie pattern with Origin/Referer checks. Safe methods mint and set a cookie; unsafe methods are rejected (403) when Origin and Referer are both missing, the token is missing, or the token does not match. Secure defaults to true (HTTPS-only cookie). See docs/errors.md to route rejections through an ErrorWriter.
  • Compression(cfg) — gzip responses above MinSize for clients that accept gzip. Never compresses text/event-stream (SSE stays live) and forwards WebSocket hijacks untouched.

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"
)

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 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 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 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 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 {
	// CookieName is the name of the CSRF cookie. Default is "_csrf".
	CookieName string

	// HeaderName is the name of the CSRF header. Default is "X-CSRF-Token".
	HeaderName 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

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

CSRFConfig configures CSRF middleware.

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 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 a CSRF middleware implementing the double-submit cookie pattern with Origin/Referer validation. Safe methods (GET/HEAD/OPTIONS) mint and set a CSRF cookie when absent; unsafe methods must present a matching header. For unsafe methods, a request with neither an Origin nor a Referer header is rejected (fail closed) because those methods are exactly what CSRF protects.

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 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, and request ID. Uses route pattern when available (Go 1.22+ ServeMux), otherwise "unknown".

type Option

type Option func(*options)

Option configures a Server at construction time.

func WithAddress

func WithAddress(address string) Option

WithAddress sets the listen address.

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 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 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.

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 {
	Address              string        `json:"address,omitempty" yaml:"address,omitempty" env:"ADDRESS" flag:"http-address"`
	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