Skip to documentation
Documentation navigation

Documentation navigation

Documentation / guides

Create a server

Construct and bind a Harness HTTP server safely.

developer

Build the handler first, then ask serve.Server to validate the listen address and return a configured *http.Server. Server does not call Listen or Serve; the application owns when serving starts and how it handles the returned server’s lifecycle.

Handler construction

The complete and read-only constructors are:

func Handler[S LiveSession, O any](rig Rig[S, O], reads Reader, opts ...Option) http.Handler
func ReadHandler(reads Reader, opts ...Option) http.Handler

type Option func(*config)
func WithAuth(authn func(*http.Request) error) Option
func WithMaxBodyBytes(n int64) Option

Handler wires the ten routes. ReadHandler wires only capabilities, list, status, and journal. WithAuth ignores a nil callback. WithMaxBodyBytes ignores zero and negative values, retaining the 1 MiB default. A nil option in the variadic list is ignored.

The Rig generic keeps the concrete session type at the composition root:

func handler(rig serve.Rig[*liveSession, sessionOption], reader serve.Reader) http.Handler {
	// `authenticate` returns nil only after the request identity is trusted.
	return serve.Handler(rig, reader,
		serve.WithAuth(authenticate),
		serve.WithMaxBodyBytes(2<<20),
	)
}

The handler wraps the mux as auth -> body cap -> route. Authentication runs before the request body is read. Body limiting is lazy, so a route sees a read error when it attempts to decode more than the configured cap.

%%{init: {"theme":"dark"}}%%
sequenceDiagram
    participant App as composition root
    participant H as serve.Handler
    participant M as ServeMux
    participant S as serve.Server
    participant HTTP as http.Server
    App->>H: Rig, Reader, Options
    H->>M: register method plus path patterns
    H-->>App: auth-aware http.Handler
    App->>S: address, handler, ServerOptions
    S->>S: parse address and check loopback/auth
    S-->>App: configured *http.Server or typed error
    App->>HTTP: ListenAndServe or Serve

Server construction

The bind constructor and its only option are:

type ServerOption func(*serverConfig)

func WithInsecurePublicBind() ServerOption
func Server(addr string, h http.Handler, opts ...ServerOption) (*http.Server, error)

Server parses addr with net.SplitHostPort. A malformed address returns InvalidAddrError and no server. A non-loopback host with no authenticator returns PublicBindWithoutAuthError unless the caller explicitly supplies WithInsecurePublicBind().

The explicit opt-in is for a deployment where an authenticating proxy or mesh sidecar is the trust boundary. It does not install authentication itself.

func start(h http.Handler) error {
	server, err := serve.Server("127.0.0.1:8080", h)
	if err != nil {
		return fmt.Errorf("configure HTTP server: %w", err)
	}
	// Server has not listened yet. The caller owns this blocking lifecycle.
	return server.ListenAndServe()
}

Hardened defaults

The returned http.Server has these values:

FieldValueReason
ReadTimeout5 sBounds the complete request read.
ReadHeaderTimeout5 sSlowloris header guard.
IdleTimeout60 sBounds idle keep-alive gaps.
MaxHeaderBytes1 MiBBounds request headers.
WriteTimeout0Keeps the long-lived events stream open.
TLSConfig.MinVersionTLS 1.2Defensive minimum if TLS is used by the deployment.

Request bodies are independently capped by WithMaxBodyBytes. The events handler clears a connection write deadline through http.ResponseController, so a server-wide write timeout cannot truncate an SSE stream.

Server detects authentication only from the handler returned directly by Handler or ReadHandler. Wrapping that value in a plain http.Handler removes the authAware proof and causes a public bind to be refused. Keep the serve handler as the value passed to Server, or preserve the same proof in a wrapper type.

Source and runnable proof

go test ./pkg/serve -run 'Test(NewConfig|With|Server|IsLoopback)'

← back to documentation